├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGES.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── example ├── generator.js ├── pet-store-1.2.json ├── pet-store-2.0.json └── specs │ └── 1.2 │ ├── pet-store.json │ ├── pet.json │ ├── store.json │ └── user.json ├── index.js ├── intl ├── cs │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ko │ └── messages.json ├── nl │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── ru │ └── messages.json ├── tr │ └── messages.json ├── zh-Hans │ └── messages.json └── zh-Hant │ └── messages.json ├── lib ├── codegen │ ├── generator-base.js │ ├── generator-v1.2.js │ ├── generator-v2.js │ ├── json-schema.js │ ├── model-template.js │ ├── model.ejs │ └── spec-converter.js └── specgen │ ├── model-helper.js │ ├── route-helper.js │ ├── schema-builder.js │ ├── swagger-spec-generator.js │ ├── tag-builder.js │ ├── type-converter.js │ └── type-registry.js ├── package.json └── test ├── codegen ├── json-schema.test.js ├── note.json ├── pet-expanded.json ├── pet-with-embedded-schema.json ├── pet-with-refs.json ├── pet-with-special-names.json ├── pet-without-tags.json ├── swagger-v12.test.js └── swagger-v2.test.js ├── mocha.opts └── specgen ├── fixtures └── dummy-swagger-ui │ ├── index.html │ └── swagger-ui.js ├── model-helper.test.js ├── route-helper.test.js ├── schema-builder.test.js ├── swagger-spec-generator.test.js └── tag-builder.test.js /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-swagger/48add01db01129d35da721529d89708e27391bc9/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback", 3 | "rules": { 4 | "max-len": ["error", 90, 4, { 5 | "ignoreComments": true, 6 | "ignoreUrls": true, 7 | "ignorePattern": "^\\s*var\\s.+=\\s*(require\\s*\\()|(/)" 8 | }] 9 | } 10 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | # Description/Steps to reproduce 11 | 12 | 16 | 17 | # Link to reproduction sandbox 18 | 19 | 24 | 25 | # Expected result 26 | 27 | 30 | 31 | # Additional information 32 | 33 | 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | #### Related issues 5 | 6 | 12 | 13 | - connect to 14 | 15 | ### Checklist 16 | 17 | 22 | 23 | - [ ] New tests added or existing tests modified to cover all changes 24 | - [ ] Code conforms with the [style 25 | guide](http://loopback.io/doc/en/contrib/style-guide.html) 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: > 21 | This issue has been closed due to continued inactivity. Thank you for your understanding. 22 | If you believe this to be in error, please contact one of the code owners, 23 | listed in the `CODEOWNERS` file at the top-level of this repository. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 'node_js' 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2020-04-28, Version 5.9.0 2 | ========================= 3 | 4 | * include relations property (Matteo Padovano) 5 | 6 | 7 | 2020-03-06, Version 5.8.1 8 | ========================= 9 | 10 | * Update LTS status in README (Miroslav Bajtoš) 11 | 12 | * chore: update copyright year (Diana Lau) 13 | 14 | 15 | 2019-10-15, Version 5.8.0 16 | ========================= 17 | 18 | * Fix handling of http.errorStatus (Matthias Tylkowski) 19 | 20 | * Add Node.js 12.x to Travis CI config (Miroslav Bajtoš) 21 | 22 | * Drop support for Node.js 6.x (Miroslav Bajtoš) 23 | 24 | * chore: update copyrights years (Agnes Lin) 25 | 26 | 27 | 2019-04-10, Version 5.7.3 28 | ========================= 29 | 30 | * chore: upgrade loopback version (Raymond Feng) 31 | 32 | * Allow tag or param names to contain '-' or spaces (Raymond Feng) 33 | 34 | * fix: update lodash (jannyHou) 35 | 36 | 37 | 2019-01-07, Version 5.7.2 38 | ========================= 39 | 40 | * Fix route helper to ignore status/header retvals (Y.Shing) 41 | 42 | 43 | 2018-10-18, Version 5.7.1 44 | ========================= 45 | 46 | * README: update LTS status (Miroslav Bajtoš) 47 | 48 | 49 | 2018-08-30, Version 5.7.0 50 | ========================= 51 | 52 | * specgen: Retrieve description from package.json (Melle Boersma) 53 | 54 | * Upgrade eslint-config-loopback + fix formatting (Miroslav Bajtoš) 55 | 56 | 57 | 2018-07-10, Version 5.6.0 58 | ========================= 59 | 60 | * specgen: emit correct non-root return types (Dan Jarvis) 61 | 62 | * Update mocha & chai to latest (Miroslav Bajtoš) 63 | 64 | * Disable package-lock feature of npm (Miroslav Bajtoš) 65 | 66 | * Update eslint + config to latest (Miroslav Bajtoš) 67 | 68 | * Update strong-globalize and debug to latest (Miroslav Bajtoš) 69 | 70 | * Travis: add Node.js 8.x and 10.x (Miroslav Bajtoš) 71 | 72 | * Drop support for Node.js 4.x (Miroslav Bajtoš) 73 | 74 | * [WebFM] cs/pl/ru translation (candytangnb) 75 | 76 | 77 | 2017-12-04, Version 5.5.0 78 | ========================= 79 | 80 | * Improve support for $ref (Raymond Feng) 81 | 82 | 83 | 2017-11-21, Version 5.4.0 84 | ========================= 85 | 86 | * Handle embedded schemas for parameter/response (Raymond Feng) 87 | 88 | 89 | 2017-11-21, Version 5.3.0 90 | ========================= 91 | 92 | * Add support for examples in models and responses (Zak Barbuto) 93 | 94 | 95 | 2017-11-20, Version 5.2.2 96 | ========================= 97 | 98 | * Improve swagger mapping (Raymond Feng) 99 | 100 | * chore: update license (Diana Lau) 101 | 102 | 103 | 2017-10-27, Version 5.2.1 104 | ========================= 105 | 106 | * Fix file upload attachments (Zak Barbuto) 107 | 108 | 109 | 2017-10-27, Version 5.2.0 110 | ========================= 111 | 112 | * feat(route-helper): Add 'documented' flag for hiding params (Samuel Reed) 113 | 114 | 115 | 2017-10-13, Version 5.1.0 116 | ========================= 117 | 118 | * update strong-globalize to 3.1.0 (shimks) 119 | 120 | * CODEOWNERS: add zbarbuto (Miroslav Bajtoš) 121 | 122 | * update globalize string (Diana Lau) 123 | 124 | * fix basePath, add store.json (wing328) 125 | 126 | 127 | 2017-09-05, Version 5.0.0 128 | ========================= 129 | 130 | * Enable 'updateOnly' feature based on generateOperationScopedModels (#99) (Rashmi Hunt) 131 | 132 | * Bump version to 5.0.0 (Raymond Feng) 133 | 134 | * Set required to be true for path parameters (Raymond Feng) 135 | 136 | 137 | 2017-08-22, Version 4.2.0 138 | ========================= 139 | 140 | * Support updateOnly feature (#92) (Rashmi Hunt) 141 | 142 | * Add stalebot configuration (Kevin Delisle) 143 | 144 | * Update Issue and PR Templates (#95) (Sakib Hasan) 145 | 146 | 147 | 2017-08-14, Version 4.1.0 148 | ========================= 149 | 150 | * Allow externalDocs and custom names for models (Zak Barbuto) 151 | 152 | * Add support for file-type parameters (Zak Barbuto) 153 | 154 | * Add community maintainers to CODEOWNERS (Miroslav Bajtoš) 155 | 156 | * Add CODEOWNER file (Diana Lau) 157 | 158 | * Update eslint config to the latest (Miroslav Bajtoš) 159 | 160 | 161 | 2017-04-27, Version 4.0.3 162 | ========================= 163 | 164 | * type-registry: add DateString type (Kevin Delisle) 165 | 166 | * Travis CI config (Kevin Delisle) 167 | 168 | 169 | 2017-03-02, Version 4.0.2 170 | ========================= 171 | 172 | * Allow swagger 1.2 (Raymond Feng) 173 | 174 | 175 | 2017-03-02, Version 4.0.1 176 | ========================= 177 | 178 | * Use a default model is added if no tags found (Raymond Feng) 179 | 180 | * Replicate new issue_template from loopback (Siddhi Pai) 181 | 182 | * Replicate issue_template from loopback repo (Siddhi Pai) 183 | 184 | 185 | 2017-02-01, Version 4.0.0 186 | ========================= 187 | 188 | * Bump version to 4.0.0 (Raymond Feng) 189 | 190 | * Remove .npmignore (Raymond Feng) 191 | 192 | * Enhance Swagger to LoopBack mapping for code gen (Raymond Feng) 193 | 194 | * Upgrade eslint-config to 7.x, eslint to 3.x (Miroslav Bajtoš) 195 | 196 | 197 | 2016-12-22, Version 3.0.1 198 | ========================= 199 | 200 | * Omit null default values from schema (Heath Morrison) 201 | 202 | 203 | 2016-12-21, Version 3.0.0 204 | ========================= 205 | 206 | * fix: package.json to reduce vulnerabilities (Ryan Graham) 207 | 208 | * Update paid support URL (Siddhi Pai) 209 | 210 | * Adjust route parameters sequence (Tao Yuan) 211 | 212 | * Start 3.x + drop support for Node v0.10/v0.12 (siddhipai) 213 | 214 | * Drop support for Node v0.10 and v0.12 (Siddhi Pai) 215 | 216 | * Start the development of the next major version (Siddhi Pai) 217 | 218 | * Coerce form to formData in accepts.http.source (Simon Ho) 219 | 220 | * Fix date output format to "date-time" (Stefan B) 221 | 222 | 223 | 2016-10-13, Version 2.8.0 224 | ========================= 225 | 226 | * Update pt translation file (Candy) 227 | 228 | * Update ja translation file (Candy) 229 | 230 | * Update translation files - round#2 (Candy) 231 | 232 | * Add translated files (gunjpan) 233 | 234 | * Update deps to loopback 3.0.0 RC (Miroslav Bajtoš) 235 | 236 | * Remove juggler as a dependency (Amir Jafarian) 237 | 238 | 239 | 2016-09-13, Version 2.7.0 240 | ========================= 241 | 242 | * Allow setting null on error's responseModel (Alex Plescan) 243 | 244 | * Use juggler@3 and loopback@3 for testing (Amir Jafarian) 245 | 246 | 247 | 2016-09-07, Version 2.6.0 248 | ========================= 249 | 250 | * Allow object arguments to provide Model (Miroslav Bajtoš) 251 | 252 | * Expose object types as nested properties (Carl Fürstenberg) 253 | 254 | 255 | 2016-09-05, Version 2.5.0 256 | ========================= 257 | 258 | * Add `patch` verb in test (Amir Jafarian) 259 | 260 | * Add globalization. (Richard Pringle) 261 | 262 | * Update URLs in CONTRIBUTING.md (#49) (Ryan Graham) 263 | 264 | * Add doc root (jannyHou) 265 | 266 | * Add custom definition to swagger file (jannyHou) 267 | 268 | 269 | 2016-05-06, Version 2.4.3 270 | ========================= 271 | 272 | * update copyright notices and license (Ryan Graham) 273 | 274 | 275 | 2016-04-13, Version 2.4.2 276 | ========================= 277 | 278 | * Do not generate required if no item is present (Raymond Feng) 279 | 280 | * Fix linting errors (Amir Jafarian) 281 | 282 | * Auto-update by eslint --fix (Amir Jafarian) 283 | 284 | * Add eslint infrastructure (Amir Jafarian) 285 | 286 | 287 | 2016-04-07, Version 2.4.1 288 | ========================= 289 | 290 | * Disable the warning as it prints messages during loopback scaffolding (Raymond Feng) 291 | 292 | 293 | 2016-04-07, Version 2.4.0 294 | ========================= 295 | 296 | * improve scheme generation for return object (hideya kawahara) 297 | 298 | * Support default status codes (Candy) 299 | 300 | 301 | 2016-03-19, Version 2.3.2 302 | ========================= 303 | 304 | * Handle {id} and . in operation id (Raymond Feng) 305 | 306 | 307 | 2016-03-03, Version 2.3.1 308 | ========================= 309 | 310 | * Handle extensions under paths (Raymond Feng) 311 | 312 | 313 | 2016-02-09, Version 2.3.0 314 | ========================= 315 | 316 | * Make type geopoint case insensitive (Candy) 317 | 318 | * Treat property as type 'any' if not specified (Candy) 319 | 320 | * Exclude definition that are not referenced (Candy) 321 | 322 | * Fix handling of allOf when generating models (Gari Singh) 323 | 324 | 325 | 2015-12-02, Version 2.2.3 326 | ========================= 327 | 328 | * specgen: fix the definition of GeoPoint type (Miroslav Bajtoš) 329 | 330 | * Fix: hidden models referenced by hidden models (Miroslav Bajtoš) 331 | 332 | * Add GeoPoint support to explorer. (Candy) 333 | 334 | 335 | 2015-11-24, Version 2.2.2 336 | ========================= 337 | 338 | * specgen: ensure operation ids are unique (Miroslav Bajtoš) 339 | 340 | * Fix typo (Candy) 341 | 342 | * Refer to licenses with a link (Sam Roberts) 343 | 344 | 345 | 2015-11-03, Version 2.2.1 346 | ========================= 347 | 348 | * type-registry: code cleanup (Miroslav Bajtoš) 349 | 350 | * Register ObjectID for MongoDB, fix cause warning: Swagger: skipping unknown type "ObjectID" (Van-Duyet Le) 351 | 352 | * map ObjectID to string type (Clark Wang) 353 | 354 | 355 | 2015-10-14, Version 2.2.0 356 | ========================= 357 | 358 | * Add support for array types/refs (Raymond Feng) 359 | 360 | * Reformat the code (Raymond Feng) 361 | 362 | 363 | 2015-09-29, Version 2.1.2 364 | ========================= 365 | 366 | * Skip null values for length (Raymond Feng) 367 | 368 | * Use strongloop conventions for licensing (Sam Roberts) 369 | 370 | 371 | 2015-09-04, Version 2.1.1 372 | ========================= 373 | 374 | * README: link to loopback-explorer (Miroslav Bajtoš) 375 | 376 | * Remove the Labs label (Miroslav Bajtoš) 377 | 378 | 379 | 2015-09-03, Version 2.1.0 380 | ========================= 381 | 382 | * Add swagger-spec generator from loopback-explorer (Miroslav Bajtoš) 383 | 384 | * Update dependencies (Miroslav Bajtoš) 385 | 386 | * Move code generator to "codegen" subfolder (Miroslav Bajtoš) 387 | 388 | * Update package.json (Rand McKinney) 389 | 390 | * Update README.md (Rand McKinney) 391 | 392 | * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) 393 | 394 | 395 | 2014-12-18, Version 2.0.0 396 | ========================= 397 | 398 | * Bump version (Raymond Feng) 399 | 400 | * Fix the spec version for 2.0 (Raymond Feng) 401 | 402 | * Add contribution guidelines (Ryan Graham) 403 | 404 | 405 | 2014-09-06, Version 1.0.1 406 | ========================= 407 | 408 | * Bump version (Raymond Feng) 409 | 410 | * Tidy up the code generation (Raymond Feng) 411 | 412 | 413 | 2014-09-05, Version 1.0.0 414 | ========================= 415 | 416 | * Bump version (Raymond Feng) 417 | 418 | * Enhance tests (Raymond Feng) 419 | 420 | * Enhance type mapping (Raymond Feng) 421 | 422 | 423 | 2014-09-05, Version 1.0.0-beta2 424 | =============================== 425 | 426 | * Bump version (Raymond Feng) 427 | 428 | * Tidy up remoting metadata (Raymond Feng) 429 | 430 | 431 | 2014-09-03, Version 1.0.0-beta1 432 | =============================== 433 | 434 | * First release! 435 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precendence. 4 | 5 | * @bajtos @STRML @zbarbuto 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `loopback-swagger`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `loopback-swagger` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-swagger) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2014,2017. All Rights Reserved. 2 | Node module: loopback-swagger 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-swagger 2 | 3 | **⚠️ LoopBack 3 is in Maintenance LTS mode, only critical bugs and critical 4 | security fixes will be provided. (See 5 | [Module Long Term Support Policy](#module-long-term-support-policy) below.)** 6 | 7 | We urge all LoopBack 3 users to migrate their applications to LoopBack 4 as 8 | soon as possible. Refer to our 9 | [Migration Guide](https://loopback.io/doc/en/lb4/migration-overview.html) 10 | for more information on how to upgrade. 11 | 12 | ## Overview 13 | 14 | Utilities to transform between Swagger API specs and LoopBack remoting metadata. 15 | 16 | This is an internal module used by the following user-facing tools: 17 | 18 | - [slc loopback:swagger](https://docs.strongloop.com/display/LB/Swagger+generator) 19 | - [loopback-explorer](https://github.com/strongloop/loopback-explorer) 20 | 21 | ## Module Long Term Support Policy 22 | 23 | This module adopts the [ 24 | Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, 25 | with the following End Of Life (EOL) dates: 26 | 27 | | Version | Status | Published | EOL | 28 | | ------- | --------------- | --------- | -------- | 29 | | 5.x | Maintenance LTS | Sep 2017 | Dec 2020 | 30 | | 4.x | End-of-Life | Feb 2017 | Apr 2019 | 31 | | 3.x | End-of-Life | Dec 2016 | Apr 2019 | 32 | 33 | Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html). 34 | -------------------------------------------------------------------------------- /example/generator.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var util = require('util'); 9 | var helper = require('../index'); 10 | 11 | var petStoreV2Spec = require('./pet-store-2.0.json'); 12 | var code = helper.generateRemoteMethods(petStoreV2Spec, {modelName: 'Store'}); 13 | console.log(code); 14 | 15 | var petStoreV12Spec = require('./pet-store-1.2.json'); 16 | code = helper.generateRemoteMethods(petStoreV12Spec); 17 | console.log(code); 18 | 19 | console.log('\nModels v2 -------------\n'); 20 | var models = helper.generateModels(petStoreV2Spec); 21 | console.log(util.inspect(models, {depth: null})); 22 | 23 | console.log('\nModels v1.2 -------------\n'); 24 | models = helper.generateModels(petStoreV12Spec); 25 | console.log(util.inspect(models, {depth: null})); 26 | -------------------------------------------------------------------------------- /example/pet-store-1.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "1.0.0", 3 | "swaggerVersion": "1.2", 4 | "basePath": "http://petstore.swagger.wordnik.com/api", 5 | "resourcePath": "/pet", 6 | "produces": ["application/json", "application/xml", "text/plain", "text/html"], 7 | "apis": [ 8 | { 9 | "path": "/pet/{petId}", 10 | "operations": [ 11 | { 12 | "method": "GET", 13 | "summary": "Find pet by ID", 14 | "notes": "Returns a pet based on ID", 15 | "type": "Pet", 16 | "nickname": "getPetById", 17 | "authorizations": {}, 18 | "parameters": [ 19 | { 20 | "name": "petId", 21 | "description": "ID of pet that needs to be fetched", 22 | "required": true, 23 | "type": "integer", 24 | "format": "int64", 25 | "paramType": "path", 26 | "allowMultiple": false, 27 | "minimum": "1.0", 28 | "maximum": "100000.0" 29 | } 30 | ], 31 | "responseMessages": [ 32 | { 33 | "code": 400, 34 | "message": "Invalid ID supplied" 35 | }, 36 | { 37 | "code": 404, 38 | "message": "Pet not found" 39 | } 40 | ] 41 | }, 42 | { 43 | "method": "PATCH", 44 | "summary": "partial updates to a pet", 45 | "notes": "", 46 | "type": "array", 47 | "items": { 48 | "$ref": "Pet" 49 | }, 50 | "nickname": "partialUpdate", 51 | "produces": ["application/json", "application/xml"], 52 | "consumes": ["application/json", "application/xml"], 53 | "authorizations": { 54 | "oauth2": [ 55 | { 56 | "scope": "write:pets", 57 | "description": "modify pets in your account" 58 | } 59 | ] 60 | }, 61 | "parameters": [ 62 | { 63 | "name": "petId", 64 | "description": "ID of pet that needs to be fetched", 65 | "required": true, 66 | "type": "string", 67 | "paramType": "path", 68 | "allowMultiple": false 69 | }, 70 | { 71 | "name": "body", 72 | "description": "Pet object that needs to be added to the store", 73 | "required": true, 74 | "type": "Pet", 75 | "paramType": "body", 76 | "allowMultiple": false 77 | } 78 | ], 79 | "responseMessages": [ 80 | { 81 | "code": 400, 82 | "message": "Invalid tag value" 83 | } 84 | ] 85 | }, 86 | { 87 | "method": "POST", 88 | "summary": "Updates a pet in the store with form data", 89 | "notes": "", 90 | "type": "void", 91 | "nickname": "updatePetWithForm", 92 | "consumes": ["application/x-www-form-urlencoded"], 93 | "authorizations": { 94 | "oauth2": [ 95 | { 96 | "scope": "write:pets", 97 | "description": "modify pets in your account" 98 | } 99 | ] 100 | }, 101 | "parameters": [ 102 | { 103 | "name": "petId", 104 | "description": "ID of pet that needs to be updated", 105 | "required": true, 106 | "type": "string", 107 | "paramType": "path", 108 | "allowMultiple": false 109 | }, 110 | { 111 | "name": "name", 112 | "description": "Updated name of the pet", 113 | "required": false, 114 | "type": "string", 115 | "paramType": "form", 116 | "allowMultiple": false 117 | }, 118 | { 119 | "name": "status", 120 | "description": "Updated status of the pet", 121 | "required": false, 122 | "type": "string", 123 | "paramType": "form", 124 | "allowMultiple": false 125 | } 126 | ], 127 | "responseMessages": [ 128 | { 129 | "code": 405, 130 | "message": "Invalid input" 131 | } 132 | ] 133 | }, 134 | { 135 | "method": "DELETE", 136 | "summary": "Deletes a pet", 137 | "notes": "", 138 | "type": "void", 139 | "nickname": "deletePet", 140 | "authorizations": { 141 | "oauth2": [ 142 | { 143 | "scope": "write:pets", 144 | "description": "modify pets in your account" 145 | } 146 | ] 147 | }, 148 | "parameters": [ 149 | { 150 | "name": "petId", 151 | "description": "Pet id to delete", 152 | "required": true, 153 | "type": "string", 154 | "paramType": "path", 155 | "allowMultiple": false 156 | } 157 | ], 158 | "responseMessages": [ 159 | { 160 | "code": 400, 161 | "message": "Invalid pet value" 162 | } 163 | ] 164 | } 165 | ] 166 | }, 167 | { 168 | "path": "/pet", 169 | "operations": [ 170 | { 171 | "method": "POST", 172 | "summary": "Add a new pet to the store", 173 | "notes": "", 174 | "type": "void", 175 | "nickname": "addPet", 176 | "consumes": ["application/json", "application/xml"], 177 | "authorizations": { 178 | "oauth2": [ 179 | { 180 | "scope": "write:pets", 181 | "description": "modify pets in your account" 182 | } 183 | ] 184 | }, 185 | "parameters": [ 186 | { 187 | "name": "body", 188 | "description": "Pet object that needs to be added to the store", 189 | "required": true, 190 | "type": "Pet", 191 | "paramType": "body", 192 | "allowMultiple": false 193 | } 194 | ], 195 | "responseMessages": [ 196 | { 197 | "code": 405, 198 | "message": "Invalid input" 199 | } 200 | ] 201 | }, 202 | { 203 | "method": "PUT", 204 | "summary": "Update an existing pet", 205 | "notes": "", 206 | "type": "void", 207 | "nickname": "updatePet", 208 | "authorizations": {}, 209 | "parameters": [ 210 | { 211 | "name": "body", 212 | "description": "Pet object that needs to be updated in the store", 213 | "required": true, 214 | "type": "Pet", 215 | "paramType": "body", 216 | "allowMultiple": false 217 | } 218 | ], 219 | "responseMessages": [ 220 | { 221 | "code": 400, 222 | "message": "Invalid ID supplied" 223 | }, 224 | { 225 | "code": 404, 226 | "message": "Pet not found" 227 | }, 228 | { 229 | "code": 405, 230 | "message": "Validation exception" 231 | } 232 | ] 233 | } 234 | ] 235 | }, 236 | { 237 | "path": "/pet/findByStatus", 238 | "operations": [ 239 | { 240 | "method": "GET", 241 | "summary": "Finds Pets by status", 242 | "notes": "Multiple status values can be provided with comma seperated strings", 243 | "type": "array", 244 | "items": { 245 | "$ref": "Pet" 246 | }, 247 | "nickname": "findPetsByStatus", 248 | "authorizations": {}, 249 | "parameters": [ 250 | { 251 | "name": "status", 252 | "description": "Status values that need to be considered for filter", 253 | "defaultValue": "available", 254 | "required": true, 255 | "type": "string", 256 | "paramType": "query", 257 | "allowMultiple": true, 258 | "enum": ["available", "pending", "sold"] 259 | } 260 | ], 261 | "responseMessages": [ 262 | { 263 | "code": 400, 264 | "message": "Invalid status value" 265 | } 266 | ] 267 | } 268 | ] 269 | }, 270 | { 271 | "path": "/pet/findByTags", 272 | "operations": [ 273 | { 274 | "method": "GET", 275 | "summary": "Finds Pets by tags", 276 | "notes": "Muliple tags can be provided with comma seperated strings. Use tag1, tag2, tag3 for testing.", 277 | "type": "array", 278 | "items": { 279 | "$ref": "Pet" 280 | }, 281 | "nickname": "findPetsByTags", 282 | "authorizations": {}, 283 | "parameters": [ 284 | { 285 | "name": "tags", 286 | "description": "Tags to filter by", 287 | "required": true, 288 | "type": "string", 289 | "paramType": "query", 290 | "allowMultiple": true 291 | } 292 | ], 293 | "responseMessages": [ 294 | { 295 | "code": 400, 296 | "message": "Invalid tag value" 297 | } 298 | ], 299 | "deprecated": "true" 300 | } 301 | ] 302 | }, 303 | { 304 | "path": "/pet/uploadImage", 305 | "operations": [ 306 | { 307 | "method": "POST", 308 | "summary": "uploads an image", 309 | "notes": "", 310 | "type": "void", 311 | "nickname": "uploadFile", 312 | "consumes": ["multipart/form-data"], 313 | "authorizations": { 314 | "oauth2": [ 315 | { 316 | "scope": "write:pets", 317 | "description": "modify pets in your account" 318 | }, 319 | { 320 | "scope": "read:pets", 321 | "description": "read your pets" 322 | } 323 | ] 324 | }, 325 | "parameters": [ 326 | { 327 | "name": "additionalMetadata", 328 | "description": "Additional data to pass to server", 329 | "required": false, 330 | "type": "string", 331 | "paramType": "form", 332 | "allowMultiple": false 333 | }, 334 | { 335 | "name": "file", 336 | "description": "file to upload", 337 | "required": false, 338 | "type": "File", 339 | "paramType": "form", 340 | "allowMultiple": false 341 | } 342 | ] 343 | } 344 | ] 345 | } 346 | ], "models": { 347 | "Tag": { 348 | "id": "Tag", 349 | "properties": { 350 | "id": { 351 | "type": "integer", 352 | "format": "int64" 353 | }, 354 | "name": { 355 | "type": "string" 356 | } 357 | } 358 | }, 359 | "Pet": { 360 | "id": "Pet", 361 | "required": ["id", "name"], 362 | "properties": { 363 | "id": { 364 | "type": "integer", 365 | "format": "int64", 366 | "description": "unique identifier for the pet", 367 | "minimum": "0.0", 368 | "maximum": "100.0" 369 | }, 370 | "category": { 371 | "$ref": "Category" 372 | }, 373 | "name": { 374 | "type": "string" 375 | }, 376 | "photoUrls": { 377 | "type": "array", 378 | "items": { 379 | "type": "string" 380 | } 381 | }, 382 | "tags": { 383 | "type": "array", 384 | "items": { 385 | "$ref": "Tag" 386 | } 387 | }, 388 | "status": { 389 | "type": "string", 390 | "description": "pet status in the store", 391 | "enum": ["available", "pending", "sold"] 392 | } 393 | } 394 | }, 395 | "Category": { 396 | "id": "Category", 397 | "properties": { 398 | "id": { 399 | "type": "integer", 400 | "format": "int64" 401 | }, 402 | "name": { 403 | "type": "string" 404 | } 405 | } 406 | } 407 | }} -------------------------------------------------------------------------------- /example/specs/1.2/pet-store.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "1.0.0", 3 | "swaggerVersion": "1.2", 4 | "apis": [ 5 | { 6 | "path": "/pet", 7 | "description": "Operations about pets" 8 | }, 9 | { 10 | "path": "/user", 11 | "description": "Operations about user" 12 | }, 13 | { 14 | "path": "/store", 15 | "description": "Operations about store" 16 | } 17 | ], "authorizations": { 18 | "oauth2": { 19 | "type": "oauth2", 20 | "scopes": [ 21 | { 22 | "scope": "write:pets", 23 | "description": "Modify pets in your account" 24 | }, 25 | { 26 | "scope": "read:pets", 27 | "description": "Read your pets" 28 | } 29 | ], 30 | "grantTypes": { 31 | "implicit": { 32 | "loginEndpoint": { 33 | "url": "http://petstore.swagger.wordnik.com/api/oauth/dialog" 34 | }, 35 | "tokenName": "access_token" 36 | }, 37 | "authorization_code": { 38 | "tokenRequestEndpoint": { 39 | "url": "http://petstore.swagger.wordnik.com/api/oauth/requestToken", 40 | "clientIdName": "client_id", 41 | "clientSecretName": "client_secret" 42 | }, 43 | "tokenEndpoint": { 44 | "url": "http://petstore.swagger.wordnik.com/api/oauth/token", 45 | "tokenName": "auth_code" 46 | } 47 | } 48 | } 49 | } 50 | }, "info": { 51 | "title": "Swagger Sample App", 52 | "description": "This is a sample server Petstore server. You can find out more about Swagger \n at http://swagger.wordnik.com or on irc.freenode.net, #swagger. For this sample,\n you can use the api key \"special-key\" to test the authorization filters", 53 | "termsOfServiceUrl": "http://helloreverb.com/terms/", 54 | "contact": "apiteam@wordnik.com", 55 | "license": "Apache 2.0", 56 | "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html" 57 | }} -------------------------------------------------------------------------------- /example/specs/1.2/pet.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "1.0.0", 3 | "swaggerVersion": "1.2", 4 | "basePath": "http://petstore.swagger.io/api", 5 | "resourcePath": "/pet", 6 | "produces": ["application/json", "application/xml", "text/plain", "text/html"], 7 | "apis": [ 8 | { 9 | "path": "/pet/{petId}", 10 | "operations": [ 11 | { 12 | "method": "GET", 13 | "summary": "Find pet by ID", 14 | "notes": "Returns a pet based on ID", 15 | "type": "Pet", 16 | "nickname": "getPetById", 17 | "authorizations": {}, 18 | "parameters": [ 19 | { 20 | "name": "petId", 21 | "description": "ID of pet that needs to be fetched", 22 | "required": true, 23 | "type": "integer", 24 | "format": "int64", 25 | "paramType": "path", 26 | "allowMultiple": false, 27 | "minimum": "1.0", 28 | "maximum": "100000.0" 29 | } 30 | ], 31 | "responseMessages": [ 32 | { 33 | "code": 400, 34 | "message": "Invalid ID supplied" 35 | }, 36 | { 37 | "code": 404, 38 | "message": "Pet not found" 39 | } 40 | ] 41 | }, 42 | { 43 | "method": "POST", 44 | "summary": "Updates a pet in the store with form data", 45 | "notes": "", 46 | "type": "void", 47 | "nickname": "updatePetWithForm", 48 | "consumes": ["application/x-www-form-urlencoded"], 49 | "authorizations": { 50 | "oauth2": [ 51 | { 52 | "scope": "write:pets", 53 | "description": "modify pets in your account" 54 | } 55 | ] 56 | }, 57 | "parameters": [ 58 | { 59 | "name": "petId", 60 | "description": "ID of pet that needs to be updated", 61 | "required": true, 62 | "type": "string", 63 | "paramType": "path", 64 | "allowMultiple": false 65 | }, 66 | { 67 | "name": "name", 68 | "description": "Updated name of the pet", 69 | "required": false, 70 | "type": "string", 71 | "paramType": "form", 72 | "allowMultiple": false 73 | }, 74 | { 75 | "name": "status", 76 | "description": "Updated status of the pet", 77 | "required": false, 78 | "type": "string", 79 | "paramType": "form", 80 | "allowMultiple": false 81 | } 82 | ], 83 | "responseMessages": [ 84 | { 85 | "code": 405, 86 | "message": "Invalid input" 87 | } 88 | ] 89 | }, 90 | { 91 | "method": "DELETE", 92 | "summary": "Deletes a pet", 93 | "notes": "", 94 | "type": "void", 95 | "nickname": "deletePet", 96 | "authorizations": { 97 | "oauth2": [ 98 | { 99 | "scope": "write:pets", 100 | "description": "modify pets in your account" 101 | } 102 | ] 103 | }, 104 | "parameters": [ 105 | { 106 | "name": "petId", 107 | "description": "Pet id to delete", 108 | "required": true, 109 | "type": "string", 110 | "paramType": "path", 111 | "allowMultiple": false 112 | } 113 | ], 114 | "responseMessages": [ 115 | { 116 | "code": 400, 117 | "message": "Invalid pet value" 118 | } 119 | ] 120 | }, 121 | { 122 | "method": "PATCH", 123 | "summary": "partial updates to a pet", 124 | "notes": "", 125 | "type": "array", 126 | "items": { 127 | "$ref": "Pet" 128 | }, 129 | "nickname": "partialUpdate", 130 | "produces": ["application/json", "application/xml"], 131 | "consumes": ["application/json", "application/xml"], 132 | "authorizations": { 133 | "oauth2": [ 134 | { 135 | "scope": "write:pets", 136 | "description": "modify pets in your account" 137 | } 138 | ] 139 | }, 140 | "parameters": [ 141 | { 142 | "name": "petId", 143 | "description": "ID of pet that needs to be fetched", 144 | "required": true, 145 | "type": "string", 146 | "paramType": "path", 147 | "allowMultiple": false 148 | }, 149 | { 150 | "name": "body", 151 | "description": "Pet object that needs to be added to the store", 152 | "required": true, 153 | "type": "Pet", 154 | "paramType": "body", 155 | "allowMultiple": false 156 | } 157 | ], 158 | "responseMessages": [ 159 | { 160 | "code": 400, 161 | "message": "Invalid tag value" 162 | } 163 | ] 164 | } 165 | ] 166 | }, 167 | { 168 | "path": "/pet", 169 | "operations": [ 170 | { 171 | "method": "POST", 172 | "summary": "Add a new pet to the store", 173 | "notes": "", 174 | "type": "void", 175 | "nickname": "addPet", 176 | "consumes": ["application/json", "application/xml"], 177 | "authorizations": { 178 | "oauth2": [ 179 | { 180 | "scope": "write:pets", 181 | "description": "modify pets in your account" 182 | } 183 | ] 184 | }, 185 | "parameters": [ 186 | { 187 | "name": "body", 188 | "description": "Pet object that needs to be added to the store", 189 | "required": true, 190 | "type": "Pet", 191 | "paramType": "body", 192 | "allowMultiple": false 193 | } 194 | ], 195 | "responseMessages": [ 196 | { 197 | "code": 405, 198 | "message": "Invalid input" 199 | } 200 | ] 201 | }, 202 | { 203 | "method": "PUT", 204 | "summary": "Update an existing pet", 205 | "notes": "", 206 | "type": "void", 207 | "nickname": "updatePet", 208 | "authorizations": {}, 209 | "parameters": [ 210 | { 211 | "name": "body", 212 | "description": "Pet object that needs to be updated in the store", 213 | "required": true, 214 | "type": "Pet", 215 | "paramType": "body", 216 | "allowMultiple": false 217 | } 218 | ], 219 | "responseMessages": [ 220 | { 221 | "code": 400, 222 | "message": "Invalid ID supplied" 223 | }, 224 | { 225 | "code": 404, 226 | "message": "Pet not found" 227 | }, 228 | { 229 | "code": 405, 230 | "message": "Validation exception" 231 | } 232 | ] 233 | } 234 | ] 235 | }, 236 | { 237 | "path": "/pet/findByStatus", 238 | "operations": [ 239 | { 240 | "method": "GET", 241 | "summary": "Finds Pets by status", 242 | "notes": "Multiple status values can be provided with comma seperated strings", 243 | "type": "array", 244 | "items": { 245 | "$ref": "Pet" 246 | }, 247 | "nickname": "findPetsByStatus", 248 | "authorizations": {}, 249 | "parameters": [ 250 | { 251 | "name": "status", 252 | "description": "Status values that need to be considered for filter", 253 | "defaultValue": "available", 254 | "required": true, 255 | "type": "string", 256 | "paramType": "query", 257 | "allowMultiple": true, 258 | "enum": ["available", "pending", "sold"] 259 | } 260 | ], 261 | "responseMessages": [ 262 | { 263 | "code": 400, 264 | "message": "Invalid status value" 265 | } 266 | ] 267 | } 268 | ] 269 | }, 270 | { 271 | "path": "/pet/findByTags", 272 | "operations": [ 273 | { 274 | "method": "GET", 275 | "summary": "Finds Pets by tags", 276 | "notes": "Muliple tags can be provided with comma seperated strings. Use tag1, tag2, tag3 for testing.", 277 | "type": "array", 278 | "items": { 279 | "$ref": "Pet" 280 | }, 281 | "nickname": "findPetsByTags", 282 | "authorizations": {}, 283 | "parameters": [ 284 | { 285 | "name": "tags", 286 | "description": "Tags to filter by", 287 | "required": true, 288 | "type": "string", 289 | "paramType": "query", 290 | "allowMultiple": true 291 | } 292 | ], 293 | "responseMessages": [ 294 | { 295 | "code": 400, 296 | "message": "Invalid tag value" 297 | } 298 | ], 299 | "deprecated": "true" 300 | } 301 | ] 302 | }, 303 | { 304 | "path": "/pet/uploadImage", 305 | "operations": [ 306 | { 307 | "method": "POST", 308 | "summary": "uploads an image", 309 | "notes": "", 310 | "type": "void", 311 | "nickname": "uploadFile", 312 | "consumes": ["multipart/form-data"], 313 | "authorizations": { 314 | "oauth2": [ 315 | { 316 | "scope": "write:pets", 317 | "description": "modify pets in your account" 318 | }, 319 | { 320 | "scope": "read:pets", 321 | "description": "read your pets" 322 | } 323 | ] 324 | }, 325 | "parameters": [ 326 | { 327 | "name": "additionalMetadata", 328 | "description": "Additional data to pass to server", 329 | "required": false, 330 | "type": "string", 331 | "paramType": "form", 332 | "allowMultiple": false 333 | }, 334 | { 335 | "name": "file", 336 | "description": "file to upload", 337 | "required": false, 338 | "type": "File", 339 | "paramType": "form", 340 | "allowMultiple": false 341 | } 342 | ] 343 | } 344 | ] 345 | } 346 | ], "models": { 347 | "Tag": { 348 | "id": "Tag", 349 | "properties": { 350 | "id": { 351 | "type": "integer", 352 | "format": "int64" 353 | }, 354 | "name": { 355 | "type": "string" 356 | } 357 | } 358 | }, 359 | "Pet": { 360 | "id": "Pet", 361 | "required": ["id", "name"], 362 | "properties": { 363 | "id": { 364 | "type": "integer", 365 | "format": "int64", 366 | "description": "unique identifier for the pet", 367 | "minimum": "0.0", 368 | "maximum": "100.0" 369 | }, 370 | "category": { 371 | "$ref": "Category" 372 | }, 373 | "name": { 374 | "type": "string" 375 | }, 376 | "photoUrls": { 377 | "type": "array", 378 | "items": { 379 | "type": "string" 380 | } 381 | }, 382 | "tags": { 383 | "type": "array", 384 | "items": { 385 | "$ref": "Tag" 386 | } 387 | }, 388 | "status": { 389 | "type": "string", 390 | "description": "pet status in the store", 391 | "enum": ["available", "pending", "sold"] 392 | } 393 | } 394 | }, 395 | "Category": { 396 | "id": "Category", 397 | "properties": { 398 | "id": { 399 | "type": "integer", 400 | "format": "int64" 401 | }, 402 | "name": { 403 | "type": "string" 404 | } 405 | } 406 | } 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /example/specs/1.2/store.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "1.0.0", 3 | "swaggerVersion": "1.2", 4 | "basePath": "http://petstore.swagger.io/api", 5 | "resourcePath": "/store", 6 | "produces": [ 7 | "application/json" 8 | ], 9 | "apis": [ 10 | { 11 | "path": "/store/order/{orderId}", 12 | "operations": [ 13 | { 14 | "method": "GET", 15 | "summary": "Find purchase order by ID", 16 | "notes": "For valid response try integer IDs with value <= 5. Anything above 5 or non-integers will generate API errors", 17 | "type": "Order", 18 | "nickname": "getOrderById", 19 | "authorizations": {}, 20 | "parameters": [ 21 | { 22 | "name": "orderId", 23 | "description": "ID of pet that needs to be fetched", 24 | "required": true, 25 | "type": "string", 26 | "paramType": "path" 27 | } 28 | ], 29 | "responseMessages": [ 30 | { 31 | "code": 400, 32 | "message": "Invalid ID supplied" 33 | }, 34 | { 35 | "code": 404, 36 | "message": "Order not found" 37 | } 38 | ] 39 | }, 40 | { 41 | "method": "DELETE", 42 | "summary": "Delete purchase order by ID", 43 | "notes": "For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors", 44 | "type": "void", 45 | "nickname": "deleteOrder", 46 | "authorizations": { 47 | "oauth2": [ 48 | { 49 | "scope": "test:anything", 50 | "description": "anything" 51 | } 52 | ] 53 | }, 54 | "parameters": [ 55 | { 56 | "name": "orderId", 57 | "description": "ID of the order that needs to be deleted", 58 | "required": true, 59 | "type": "string", 60 | "paramType": "path" 61 | } 62 | ], 63 | "responseMessages": [ 64 | { 65 | "code": 400, 66 | "message": "Invalid ID supplied" 67 | }, 68 | { 69 | "code": 404, 70 | "message": "Order not found" 71 | } 72 | ] 73 | } 74 | ] 75 | }, 76 | { 77 | "path": "/store/order", 78 | "operations": [ 79 | { 80 | "method": "POST", 81 | "summary": "Place an order for a pet", 82 | "notes": "", 83 | "type": "void", 84 | "nickname": "placeOrder", 85 | "authorizations": { 86 | "oauth2": [ 87 | { 88 | "scope": "test:anything", 89 | "description": "anything" 90 | } 91 | ] 92 | }, 93 | "parameters": [ 94 | { 95 | "name": "body", 96 | "description": "order placed for purchasing the pet", 97 | "required": true, 98 | "type": "Order", 99 | "paramType": "body" 100 | } 101 | ], 102 | "responseMessages": [ 103 | { 104 | "code": 400, 105 | "message": "Invalid order" 106 | } 107 | ] 108 | } 109 | ] 110 | } 111 | ], 112 | "models": { 113 | "Order": { 114 | "id": "Order", 115 | "description": "an order in the system", 116 | "properties": { 117 | "id": { 118 | "type": "integer", 119 | "format": "int64" 120 | }, 121 | "petId": { 122 | "type": "integer", 123 | "format": "int64" 124 | }, 125 | "quantity": { 126 | "type": "integer", 127 | "format": "int32" 128 | }, 129 | "status": { 130 | "type": "string", 131 | "description": "Order Status", 132 | "enum": [ 133 | "placed", 134 | " approved", 135 | " delivered" 136 | ] 137 | }, 138 | "shipDate": { 139 | "type": "string", 140 | "format": "date-time" 141 | } 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /example/specs/1.2/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "1.0.0", 3 | "swaggerVersion": "1.2", 4 | "basePath": "http://petstore.swagger.io/api", 5 | "resourcePath": "/user", 6 | "produces": ["application/json"], 7 | "apis": [ 8 | { 9 | "path": "/user/{username}", 10 | "operations": [ 11 | { 12 | "method": "PUT", 13 | "summary": "Updated user", 14 | "notes": "This can only be done by the logged in user.", 15 | "type": "void", 16 | "nickname": "updateUser", 17 | "authorizations": { 18 | "oauth2": [ 19 | { 20 | "scope": "test:anything", 21 | "description": "anything" 22 | } 23 | ] 24 | }, 25 | "parameters": [ 26 | { 27 | "name": "username", 28 | "description": "name that need to be deleted", 29 | "required": true, 30 | "type": "string", 31 | "paramType": "path", 32 | "allowMultiple": false 33 | }, 34 | { 35 | "name": "body", 36 | "description": "Updated user object", 37 | "required": true, 38 | "type": "User", 39 | "paramType": "body", 40 | "allowMultiple": false 41 | } 42 | ], 43 | "responseMessages": [ 44 | { 45 | "code": 400, 46 | "message": "Invalid username supplied" 47 | }, 48 | { 49 | "code": 404, 50 | "message": "User not found" 51 | } 52 | ] 53 | }, 54 | { 55 | "method": "DELETE", 56 | "summary": "Delete user", 57 | "notes": "This can only be done by the logged in user.", 58 | "type": "void", 59 | "nickname": "deleteUser", 60 | "authorizations": { 61 | "oauth2": [ 62 | { 63 | "scope": "test:anything", 64 | "description": "anything" 65 | } 66 | ] 67 | }, 68 | "parameters": [ 69 | { 70 | "name": "username", 71 | "description": "The name that needs to be deleted", 72 | "required": true, 73 | "type": "string", 74 | "paramType": "path", 75 | "allowMultiple": false 76 | } 77 | ], 78 | "responseMessages": [ 79 | { 80 | "code": 400, 81 | "message": "Invalid username supplied" 82 | }, 83 | { 84 | "code": 404, 85 | "message": "User not found" 86 | } 87 | ] 88 | }, 89 | { 90 | "method": "GET", 91 | "summary": "Get user by user name", 92 | "notes": "", 93 | "type": "User", 94 | "nickname": "getUserByName", 95 | "authorizations": {}, 96 | "parameters": [ 97 | { 98 | "name": "username", 99 | "description": "The name that needs to be fetched. Use user1 for testing.", 100 | "required": true, 101 | "type": "string", 102 | "paramType": "path", 103 | "allowMultiple": false 104 | } 105 | ], 106 | "responseMessages": [ 107 | { 108 | "code": 400, 109 | "message": "Invalid username supplied" 110 | }, 111 | { 112 | "code": 404, 113 | "message": "User not found" 114 | } 115 | ] 116 | } 117 | ] 118 | }, 119 | { 120 | "path": "/user/login", 121 | "operations": [ 122 | { 123 | "method": "GET", 124 | "summary": "Logs user into the system", 125 | "notes": "", 126 | "type": "string", 127 | "nickname": "loginUser", 128 | "authorizations": {}, 129 | "parameters": [ 130 | { 131 | "name": "username", 132 | "description": "The user name for login", 133 | "required": true, 134 | "type": "string", 135 | "paramType": "query", 136 | "allowMultiple": false 137 | }, 138 | { 139 | "name": "password", 140 | "description": "The password for login in clear text", 141 | "required": true, 142 | "type": "string", 143 | "paramType": "query", 144 | "allowMultiple": false 145 | } 146 | ], 147 | "responseMessages": [ 148 | { 149 | "code": 400, 150 | "message": "Invalid username and password combination" 151 | } 152 | ] 153 | } 154 | ] 155 | }, 156 | { 157 | "path": "/user/logout", 158 | "operations": [ 159 | { 160 | "method": "GET", 161 | "summary": "Logs out current logged in user session", 162 | "notes": "", 163 | "type": "void", 164 | "nickname": "logoutUser", 165 | "authorizations": {}, 166 | "parameters": [] 167 | } 168 | ] 169 | }, 170 | { 171 | "path": "/user/createWithArray", 172 | "operations": [ 173 | { 174 | "method": "POST", 175 | "summary": "Creates list of users with given input array", 176 | "notes": "", 177 | "type": "void", 178 | "nickname": "createUsersWithArrayInput", 179 | "authorizations": { 180 | "oauth2": [ 181 | { 182 | "scope": "test:anything", 183 | "description": "anything" 184 | } 185 | ] 186 | }, 187 | "parameters": [ 188 | { 189 | "name": "body", 190 | "description": "List of user object", 191 | "required": true, 192 | "type": "array", 193 | "items": { 194 | "$ref": "User" 195 | }, 196 | "paramType": "body", 197 | "allowMultiple": false 198 | } 199 | ] 200 | } 201 | ] 202 | }, 203 | { 204 | "path": "/user/createWithList", 205 | "operations": [ 206 | { 207 | "method": "POST", 208 | "summary": "Creates list of users with given list input", 209 | "notes": "", 210 | "type": "void", 211 | "nickname": "createUsersWithListInput", 212 | "authorizations": { 213 | "oauth2": [ 214 | { 215 | "scope": "test:anything", 216 | "description": "anything" 217 | } 218 | ] 219 | }, 220 | "parameters": [ 221 | { 222 | "name": "body", 223 | "description": "List of user object", 224 | "required": true, 225 | "type": "array", 226 | "items": { 227 | "$ref": "User" 228 | }, 229 | "paramType": "body", 230 | "allowMultiple": false 231 | } 232 | ] 233 | } 234 | ] 235 | }, 236 | { 237 | "path": "/user", 238 | "operations": [ 239 | { 240 | "method": "POST", 241 | "summary": "Create user", 242 | "notes": "This can only be done by the logged in user.", 243 | "type": "void", 244 | "nickname": "createUser", 245 | "authorizations": { 246 | "oauth2": [ 247 | { 248 | "scope": "test:anything", 249 | "description": "anything" 250 | } 251 | ] 252 | }, 253 | "parameters": [ 254 | { 255 | "name": "body", 256 | "description": "Created user object", 257 | "required": true, 258 | "type": "User", 259 | "paramType": "body", 260 | "allowMultiple": false 261 | } 262 | ] 263 | } 264 | ] 265 | } 266 | ], 267 | "models": { 268 | "User": { 269 | "id": "User", 270 | "properties": { 271 | "id": { 272 | "type": "integer", 273 | "format": "int64" 274 | }, 275 | "firstName": { 276 | "type": "string" 277 | }, 278 | "username": { 279 | "type": "string" 280 | }, 281 | "lastName": { 282 | "type": "string" 283 | }, 284 | "email": { 285 | "type": "string" 286 | }, 287 | "password": { 288 | "type": "string" 289 | }, 290 | "phone": { 291 | "type": "string" 292 | }, 293 | "userStatus": { 294 | "type": "integer", 295 | "format": "int32", 296 | "description": "User Status", 297 | "enum": ["1-registered", "2-active", "3-closed"] 298 | } 299 | } 300 | } 301 | }} 302 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // Globalization 9 | var SG = require('strong-globalize'); 10 | SG.SetRootDir(__dirname); 11 | var g = SG(); 12 | 13 | var V2Generator = require('./lib/codegen/generator-v2'); 14 | var V12Generator = require('./lib/codegen/generator-v1.2'); 15 | var generateModels = require('./lib/codegen/json-schema'); 16 | var generateSwaggerSpec = require('./lib/specgen/swagger-spec-generator'); 17 | 18 | function getGenerator(spec) { 19 | var generator; 20 | if (spec && spec.swagger === '2.0') { 21 | generator = new V2Generator(); 22 | } else if (spec && spec.swaggerVersion === '1.2') { 23 | generator = new V12Generator(); 24 | } else { 25 | throw new Error(g.f('{{Swagger spec}} version is not supported')); 26 | } 27 | return generator; 28 | } 29 | 30 | /** 31 | * Generate remote methods from swagger spec 32 | * @param {Object} spec 33 | * @param {Object} options 34 | * @returns {String} 35 | */ 36 | exports.generateRemoteMethods = function(spec, options) { 37 | return getGenerator(spec).generateRemoteMethods(spec, options); 38 | }; 39 | 40 | /** 41 | * Generate remote methods for an array of operations 42 | * @param {String|Number} version 43 | * @param {String} modelName 44 | * @param {BaseOperation[]} operations 45 | * @returns {String} 46 | */ 47 | exports.generateCode = function(version, modelName, operations) { 48 | var spec = {}; 49 | if (version === '1.2') { 50 | spec.swaggerVersion = '1.2'; 51 | } else { 52 | spec.swagger = 2; 53 | } 54 | return getGenerator(spec).generateCodeForOperations(modelName, operations); 55 | }; 56 | 57 | /** 58 | * Generate model definitions 59 | * @param {Object} spec Swagger spec 60 | * @param {Object} options 61 | * @returns {Object} 62 | */ 63 | exports.generateModels = function(spec, options) { 64 | var models; 65 | if (spec && spec.swagger === '2.0') { 66 | models = spec.definitions; 67 | } else if (spec && spec.swaggerVersion === '1.2') { 68 | models = spec.models; 69 | } else { 70 | throw new Error(g.f('{{Swagger spec}} version is not supported')); 71 | } 72 | return generateModels(models, options || {}); 73 | }; 74 | 75 | exports.getGenerator = getGenerator; 76 | 77 | exports.generateSwaggerSpec = generateSwaggerSpec; 78 | -------------------------------------------------------------------------------- /intl/cs/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "Verze {{Swagger spec}} není podporována", 3 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: Přeskočení neznámého typu {0}.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Varování: Neznámý typ LDL {0}, místo toho použití \"{{any}}\" ", 5 | "ea289bffae13f78fad7794b62395008d": "Trasa existuje bez třídy: {0}", 6 | "f56715842719b19ea94d85dd1f024684": "Varování: Zjištěno více vzdálených metod ve stejném koncovém bodu HTTP. {{Swagger operation ids}} NEBUDE jedinečný." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "{{Swagger spec}}-Version wird nicht unterstützt", 3 | "f56715842719b19ea94d85dd1f024684": "Warnung: Es wurden mehrere Remote-Methoden am gleichen HTTP-Endpunkt erkannt. {{Swagger operation ids}} wird NICHT eindeutig sein.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Warnung: unbekannter LDL-Typ {0}, \"{{any}}\" wird stattdessen verwendet", 5 | "ea289bffae13f78fad7794b62395008d": "Route ohne Klasse vorhanden: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: unbekannter Typ {0} wird übersprungen." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "{{Swagger spec}} version is not supported", 3 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: skipping unknown type {0}.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Warning: unknown LDL type {0}, using \"{{any}}\" instead", 5 | "ea289bffae13f78fad7794b62395008d": "Route exists with no class: {0}", 6 | "f56715842719b19ea94d85dd1f024684": "Warning: detected multiple remote methods at the same HTTP endpoint. {{Swagger operation ids}} will NOT be unique." 7 | } 8 | -------------------------------------------------------------------------------- /intl/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "La versión de {{Swagger spec}} no está soportada", 3 | "f56715842719b19ea94d85dd1f024684": "Aviso: han detectado varios métodos remotos en el mismo punto final HTTP. Los {{Swagger operation ids}} NO serán exclusivos.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Aviso: tipo LDL desconocido {0}, se utiliza\"{{any}}\" en su lugar", 5 | "ea289bffae13f78fad7794b62395008d": "La ruta existe sin clase: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: se pasa por alto el tipo desconocido {0}." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "La version {{Swagger spec}} n'est pas prise en charge", 3 | "f56715842719b19ea94d85dd1f024684": "Avertissement : plusieurs méthodes distantes détectées sur le même noeud final HTTP. {{Swagger operation ids}} ne seront PAS uniques.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Avertissement : type LDL inconnu {0} ; \"{{any}}\" est utilisé à la place", 5 | "ea289bffae13f78fad7794b62395008d": "La route existe sans classe : {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}} : le type inconnu {0} est ignoré." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "Versione di {{Swagger spec}} non supportata", 3 | "f56715842719b19ea94d85dd1f024684": "Avvertenza: rilevati più metodi remoti nello stesso endpoint HTTP. {{Swagger operation ids}} NON potrà essere univoca.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Avvertenza: tipo LDL sconosciuto {0}, viene utilizzato \"{{any}}\"", 5 | "ea289bffae13f78fad7794b62395008d": "Presente instradamento senza classe: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: il tipo sconosciuto viene ignorato {0}." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "{{Swagger spec}} バージョンはサポートされていません", 3 | "f56715842719b19ea94d85dd1f024684": "警告: 同じ HTTP エンドポイントで複数のリモート・メソッドが検出されました。 {{Swagger operation ids}} は一意になりません。", 4 | "887507f75b7a4051e4c604f1f187d7be": "警告: 不明な LDL タイプ {0} です。代わりに \"{{any}}\" を使用します", 5 | "ea289bffae13f78fad7794b62395008d": "クラスのない経路が存在します: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: 不明なタイプ {0} をスキップします。" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "{{Swagger spec}} 버전이 지원되지 않음", 3 | "f56715842719b19ea94d85dd1f024684": "경고: 동일한 HTTP 엔드포인트에서 여러 개의 원격 메소드가 발견되었습니다. {{Swagger operation ids}}이(가) 고유하지 않습니다.", 4 | "887507f75b7a4051e4c604f1f187d7be": "경고: 알 수 없는 LDL 유형 {0}입니다. 대신 \"{{any}}\"을(룰) 사용합니다.", 5 | "ea289bffae13f78fad7794b62395008d": "클래스 없이 라우트가 있음: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: 알 수 없는 유형 {0}을(를) 건너뜁니다." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "Versie van {{Swagger spec}} wordt niet ondersteund", 3 | "f56715842719b19ea94d85dd1f024684": "Waarschuwing: Meerdere niet-lokale methoden gevonden op hetzelfde HTTP-eindpunt. {{Swagger operation ids}} is NIET uniek.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Waarschuwing: onbekend LDL-type {0}, in plaats daarvan wordt \"{{any}}\" gebruikt", 5 | "ea289bffae13f78fad7794b62395008d": "Klasse ontbreekt in route: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: Onbekend type {0} wordt overgeslagen." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "Wersja {{Swagger spec}} nie jest obsługiwana", 3 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: pomijanie nieznanego typu {0}.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Ostrzeżenie: nieznany typ LDL {0}, zamiast niego zostanie użyty typ \"{{any}}\"", 5 | "ea289bffae13f78fad7794b62395008d": "Istnieje trasa bez klasy: {0}", 6 | "f56715842719b19ea94d85dd1f024684": "Ostrzeżenie: wykryto wiele metod zdalnych w tym samym punkcie końcowym HTTP. Identyfikatory {{Swagger operation ids}} NIE będą unikalne." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "Versão de {{Swagger spec}} não é suportada", 3 | "f56715842719b19ea94d85dd1f024684": "Aviso: detectados múltiplos métodos remotos no mesmo terminal HTTP. {{Swagger operation ids}} NÃO será exclusivo.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Aviso: LDL tipo {0} desconhecido, usando \"{{any}}\" no lugar", 5 | "ea289bffae13f78fad7794b62395008d": "Rota existe sem classe: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: ignorando tipo desconhecido {0}." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "Версия {{Swagger spec}} не поддерживается", 3 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: неизвестный тип {0} пропущен.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Предупреждение: неизвестный тип LDL {0}, вместо него используется \"{{any}}\"", 5 | "ea289bffae13f78fad7794b62395008d": "Маршрут существует без класса: {0}", 6 | "f56715842719b19ea94d85dd1f024684": "Предупреждение: в одной конечной точке HTTP обнаружено несколько удаленных методов. {{Swagger operation ids}} НЕ БУДЕТ уникальным." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/tr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "{{Swagger spec}} sürümü desteklenmiyor", 3 | "f56715842719b19ea94d85dd1f024684": "Uyarı: Aynı HTTP uç noktasında birden çok uzak yöntem algılandı. {{Swagger operation ids}} benzersiz OLMAYACAK.", 4 | "887507f75b7a4051e4c604f1f187d7be": "Uyarı: LDL tipi {0} bilinmiyor, onun yerine \"{{any}}\" kullanılıyor", 5 | "ea289bffae13f78fad7794b62395008d": "Rota var, ancak sınıfı yok: {0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}: bilinmeyen {0} tipi atlanıyor." 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/zh-Hans/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "不支持 {{Swagger spec}} 版本", 3 | "f56715842719b19ea94d85dd1f024684": "警告:在相同的 HTTP 端点上检测到多个远程方法。{{Swagger operation ids}} 非唯一。", 4 | "887507f75b7a4051e4c604f1f187d7be": "警告:未知的 LDL 类型 {0},改用“{{any}}”", 5 | "ea289bffae13f78fad7794b62395008d": "存在无类的路径:{0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}:正在跳过未知类型 {0}。" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /intl/zh-Hant/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "47a65938fb7d8999530eb834c7f88bfe": "不支援 {{Swagger spec}} 版本", 3 | "f56715842719b19ea94d85dd1f024684": "警告:在相同的 HTTP 端點上偵測到多個遠端方法。{{Swagger operation ids}} 將「不是」唯一的。", 4 | "887507f75b7a4051e4c604f1f187d7be": "警告:LDL 類型 {0} 不明,改用 \"{{any}}\"", 5 | "ea289bffae13f78fad7794b62395008d": "存在的路徑上沒有類別:{0}", 6 | "8695812bf6bbddb8096a6084b3214375": "{{Swagger}}:正在跳過不明類型 {0}。" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /lib/codegen/generator-base.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var ejs = require('ejs'); 9 | var util = require('util'); 10 | var _cloneDeep = require('lodash').cloneDeep; 11 | 12 | var template = require('./model-template'); 13 | 14 | function normalizeName(name) { 15 | return name.replace(/[\-\.\s]/g, '_'); 16 | } 17 | 18 | function BaseGenerator(options) { 19 | this.options = options || {}; 20 | } 21 | 22 | function BaseOperation(op) { 23 | var copy = _cloneDeep(op || {}); 24 | for (var p in copy) { 25 | this[p] = copy[p]; 26 | } 27 | this.models = op.models; 28 | } 29 | 30 | /** 31 | * Build parameters 32 | * @returns {Array|*} 33 | */ 34 | BaseOperation.prototype.getAccepts = function() { 35 | if (this.accepts) { 36 | return this.accepts; 37 | } 38 | var accepts = this.parameters.map(this.parameter.bind(this)); 39 | this.accepts = accepts; 40 | return this.accepts; 41 | }; 42 | 43 | /** 44 | * Build remoting metadata 45 | * @returns {{isStatic: boolean}|*} 46 | */ 47 | BaseOperation.prototype.getRemoting = function() { 48 | if (this.remoting) { 49 | return this.remoting; 50 | } 51 | var isStatic = this.isStatic === undefined ? true : !!this.isStatic; 52 | var remoting = {isStatic: isStatic}; 53 | if (this.consumes) { 54 | remoting.consumes = this.consumes; 55 | } 56 | if (this.produces) { 57 | remoting.produces = this.produces; 58 | } 59 | remoting.accepts = this.getAccepts(); 60 | remoting.returns = this.getReturns(); 61 | remoting.http = { 62 | verb: this.verb, 63 | path: this.path, 64 | }; 65 | remoting.description = this.description; 66 | this.remoting = remoting; 67 | return this.remoting; 68 | }; 69 | 70 | BaseOperation.prototype.printRemoting = function() { 71 | return util.inspect(this.getRemoting(), {depth: null}); 72 | }; 73 | 74 | exports.BaseOperation = BaseOperation; 75 | exports.BaseGenerator = BaseGenerator; 76 | 77 | function getDefaultModelName(options) { 78 | options = options || {}; 79 | return options.modelName || 'SwaggerModel'; 80 | } 81 | 82 | /** 83 | * Map tags to special models as controllers for REST APIs. The spec object 84 | * will be mutated for operation.tags and root-level tags. 85 | * 86 | * @param spec {Object} The Swagger spec 87 | * @param options {Object} Options 88 | */ 89 | BaseGenerator.prototype.mapTagsToModels = function(spec, options) { 90 | options = options || {}; 91 | var defaultModelName = getDefaultModelName(options); 92 | var operations = this.getOperations(spec); 93 | var definitions = spec.definitions || spec.models; 94 | 95 | function mapTag(tag) { 96 | var modelName = normalizeName(tag); 97 | if (!definitions[modelName]) { 98 | // Add a controller to definitions so that it will be generated 99 | // as a model 100 | definitions[modelName] = { 101 | type: 'object', 102 | name: modelName, 103 | 'x-base-type': 'Model', 104 | properties: {}, 105 | }; 106 | } 107 | // Add tags to spec top level 108 | spec.tags = spec.tags || []; 109 | var found = false; 110 | for (var i = 0, n = spec.tags.length; i < n; i++) { 111 | if (spec.tags[i].name === tag) { 112 | found = true; 113 | break; 114 | } 115 | } 116 | if (!found) { 117 | spec.tags.push({name: tag}); 118 | } 119 | } 120 | 121 | /* eslint-disable one-var */ 122 | for (var path in operations) { 123 | for (var verb in operations[path]) { 124 | var op = operations[path][verb]; 125 | if (!Array.isArray(op.tags) || op.tags.length === 0) { 126 | // Default to the SwaggerModel controller 127 | op.tags = [defaultModelName]; 128 | } 129 | op.tags.forEach(mapTag); 130 | } 131 | } 132 | }; 133 | 134 | /** 135 | * Generate remote methods for a Swagger spec 136 | * @param spec {Object} The swagger spec object 137 | * @param options {Object} Options 138 | * @returns An object keyed by model names, with each value as a string for 139 | * the generated code 140 | */ 141 | BaseGenerator.prototype.generateRemoteMethods = function(spec, options) { 142 | options = options || {}; 143 | spec.definitions = spec.definitions || {}; 144 | var modelName = getDefaultModelName(options); 145 | var operations = this.getOperations(spec); 146 | 147 | // Operations by Tag 148 | var operationList = {}; 149 | 150 | function mapTag(tag) { 151 | var modelName = normalizeName(tag); 152 | var ops = operationList[modelName]; 153 | if (!ops) { 154 | ops = operationList[modelName] = []; 155 | } 156 | var copy = _cloneDeep(op); 157 | copy.models = op.models; // Keep the same ref to models 158 | copy.operationId = op.name; 159 | if (op.name && op.name.indexOf('prototype.') === 0) { 160 | copy.isStatic = false; 161 | copy.accepts = copy.accepts.filter(function(a) { 162 | return a.arg !== 'id'; 163 | }); 164 | delete copy.remoting; 165 | } 166 | ops.push(copy); 167 | } 168 | 169 | /* eslint-disable one-var */ 170 | for (var path in operations) { 171 | for (var verb in operations[path]) { 172 | var op = operations[path][verb]; 173 | if (!Array.isArray(op.tags) || op.tags.length === 0) { 174 | op.tags = [modelName]; 175 | } 176 | op.tags.forEach(mapTag); 177 | } 178 | } 179 | /* eslint-enable one-var */ 180 | var code = this.generateCodeForOperations(operationList, options); 181 | return code; 182 | }; 183 | 184 | /** 185 | * Generate code for operations 186 | * @param operations {Object} modelName ==> an array of operations for the given 187 | * model 188 | * @param options {Object} Options 189 | * @returns An object keyed by model names, with each value as a string for 190 | * the generated code 191 | */ 192 | BaseGenerator.prototype.generateCodeForOperations = 193 | function(operations, options) { 194 | var modelName = getDefaultModelName(options); 195 | var codeMap = {}; 196 | for (var m in operations) { 197 | var code = ejs.render(template, { 198 | modelName: m || modelName, 199 | operations: operations[m], 200 | }); 201 | codeMap[m] = code; 202 | } 203 | return codeMap; 204 | }; 205 | 206 | -------------------------------------------------------------------------------- /lib/codegen/generator-v1.2.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var assert = require('assert'); 9 | var util = require('util'); 10 | var base = require('./generator-base'); 11 | 12 | function V12Generator(options) { 13 | base.BaseGenerator.apply(this, arguments); 14 | } 15 | 16 | util.inherits(V12Generator, base.BaseGenerator); 17 | 18 | function V12Operation(op) { 19 | base.BaseOperation.apply(this, arguments); 20 | } 21 | 22 | util.inherits(V12Operation, base.BaseOperation); 23 | 24 | V12Operation.prototype.resolveTypeRef = function(ref) { 25 | if (typeof ref === 'string') { 26 | if (ref.indexOf('#/models/') === 0) { 27 | ref = ref.substring('#/models/'.length); 28 | } 29 | } 30 | return ref; 31 | }; 32 | /** 33 | * Convert a swagger parameter to strong-remoting argument 34 | * @param {Object} p 35 | * @returns {Object} 36 | */ 37 | V12Operation.prototype.parameter = function(p) { 38 | var type = p.type; 39 | if (p.type === 'integer') { 40 | type = 'number'; 41 | } 42 | if (p.type === 'array' && p.items) { 43 | type = [p.items.type || this.resolveTypeRef(p.items.$ref)]; 44 | } 45 | if (p.schema && p.schema.$ref) { 46 | type = this.resolveTypeRef(p.schema.$ref); 47 | } 48 | return { 49 | arg: p.name, 50 | type: type || 'any', 51 | description: p.description, 52 | required: p.required, 53 | http: { 54 | source: p.paramType, 55 | }, 56 | }; 57 | }; 58 | 59 | V12Operation.prototype.getReturns = function() { 60 | if (this.returns) { 61 | return this.returns; 62 | } 63 | var returns = [ 64 | ]; 65 | 66 | var type = this.type; 67 | if (this.type === 'integer') { 68 | type = 'number'; 69 | } 70 | if (this.type === 'array' && this.items) { 71 | type = [this.items.type || this.resolveTypeRef(this.items.$ref)]; 72 | } 73 | if (this.type && this.type !== 'void') { 74 | returns.push({ 75 | description: this.description || this.notes, 76 | type: type || 'any', 77 | arg: 'data', 78 | root: true, 79 | }); 80 | } 81 | this.errorTypes = []; 82 | this.returnType = type || 'any'; 83 | if (this.responseMessages) { 84 | for (var i = 0, n = this.responseMessages.length; i < n; i++) { 85 | var res = this.responseMessages[i]; 86 | 87 | this.errorTypes.push({ 88 | statusCode: res.code, 89 | message: res.message, 90 | }); 91 | } 92 | } 93 | this.returns = returns; 94 | return this.returns; 95 | }; 96 | 97 | V12Generator.prototype.getOperations = function(spec) { 98 | assert(spec && spec.swaggerVersion === '1.2'); 99 | // var resourcePath = spec.resourcePath; 100 | // var basePath = spec.basePath; 101 | spec.models = spec.models || {}; 102 | var models = spec.models; 103 | 104 | var operations = {}; 105 | for (var i = 0, n = spec.apis.length; i < n; i++) { 106 | var api = spec.apis[i]; 107 | var path = api.path; 108 | 109 | for (var j = 0, k = api.operations.length; j < k; j++) { 110 | var op = api.operations[j]; 111 | 112 | if (!op.parameters) { 113 | op.parameters = []; 114 | } 115 | 116 | op.consumes = op.consumes || spec.consumes; 117 | op.produces = op.produces || spec.produces; 118 | 119 | op.operationId = op.nickname; 120 | op.models = models; 121 | 122 | op.description = op.summary || op.notes; 123 | 124 | // Replace {id} with :id 125 | op.path = path.replace(/{(([^{}])+)}/g, ':$1'); 126 | op.verb = op.method && op.method.toLowerCase(); 127 | 128 | var operation = new V12Operation(op); 129 | operation.getRemoting(); 130 | 131 | operations[operation.path] = operations[operation.path] || {}; 132 | operations[operation.path][operation.verb] = operation; 133 | } 134 | } 135 | return operations; 136 | }; 137 | 138 | module.exports = V12Generator; 139 | 140 | -------------------------------------------------------------------------------- /lib/codegen/generator-v2.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var assert = require('assert'); 9 | var util = require('util'); 10 | var base = require('./generator-base'); 11 | var buildModels = require('./json-schema'); 12 | var _ = require('lodash'); 13 | 14 | function V2Generator(options) { 15 | base.BaseGenerator.apply(this, arguments); 16 | } 17 | 18 | util.inherits(V2Generator, base.BaseGenerator); 19 | 20 | function V2Operation(op) { 21 | base.BaseOperation.apply(this, arguments); 22 | } 23 | 24 | util.inherits(V2Operation, base.BaseOperation); 25 | 26 | V2Operation.prototype.resolveTypeRef = function(ref) { 27 | if (typeof ref === 'string') { 28 | if (ref.indexOf('#/definitions/') === 0) { 29 | ref = ref.substring('#/definitions/'.length); 30 | } 31 | } 32 | return ref; 33 | }; 34 | 35 | /** 36 | * Convert a swagger parameter to strong-remoting argument 37 | * @param {Object} p 38 | * @returns {Object} 39 | */ 40 | V2Operation.prototype.parameter = function(p) { 41 | var type = p.type; 42 | if (p.type === 'integer') { 43 | type = 'number'; 44 | } 45 | if (p.type === 'array' && p.items) { 46 | type = [p.items.type || this.resolveTypeRef(getRef(p.items))]; 47 | } 48 | if (p.schema && getRef(p.schema)) { 49 | type = this.resolveTypeRef(getRef(p.schema)); 50 | } else if (p.schema && p.schema.type === 'object') { 51 | type = (this.name + '_' + p.name).replace(/\./g, '_'); 52 | var schema = {}; 53 | schema[type] = p.schema; 54 | var models = buildModels(schema); 55 | this.models[type] = models[type]; 56 | return { 57 | arg: p.name, 58 | type: type || 'any', 59 | description: p.description, 60 | required: p.required, 61 | http: { 62 | source: p.in, 63 | }, 64 | }; 65 | } 66 | 67 | return { 68 | arg: p.name, 69 | type: type || 'any', 70 | description: p.description, 71 | required: p.required, 72 | http: { 73 | source: p.in, 74 | }, 75 | }; 76 | }; 77 | 78 | V2Operation.prototype.getReturns = function() { 79 | if (this.returns) { 80 | return this.returns; 81 | } 82 | var returns = []; 83 | this.errorTypes = []; 84 | this.returnType = 'any'; 85 | var modelName, model, type, code; 86 | for (code in this.responses) { 87 | var res = this.responses[code]; 88 | if (code.match(/^2\d\d$/)) { 89 | if (res.schema && getRef(res.schema)) { 90 | modelName = this.resolveTypeRef(getRef(res.schema)); 91 | model = this.models[modelName]; 92 | type = model ? modelName : 'Object'; 93 | this.returnType = type || 'any'; 94 | returns.push({ 95 | description: res.description, 96 | type: type || 'any', 97 | arg: 'data', 98 | root: true, 99 | }); 100 | } else if (res.schema && res.schema.type === 'array' && 101 | res.schema.items && 102 | getRef(res.schema.items)) { 103 | /** 104 | * schema: 105 | * type: array 106 | * items: 107 | * $ref: '#/definitions/Organization' 108 | */ 109 | modelName = this.resolveTypeRef(getRef(res.schema.items)); 110 | model = this.models[modelName]; 111 | type = model ? modelName : 'Object'; 112 | this.returnType = [type]; 113 | returns.push({ 114 | description: res.description, 115 | type: [type], 116 | arg: 'data', 117 | root: true, 118 | }); 119 | } else if (res.schema && res.schema.type === 'array' && 120 | res.schema.items) { 121 | if (res.schema.items.type === 'object') { 122 | type = (this.name + '_response_' + code).replace(/\./g, '_'); 123 | var schema = {}; 124 | schema[type] = res.schema.items; 125 | var models = buildModels(schema); 126 | this.models[type] = models[type]; 127 | } else { 128 | type = res.schema.items.type; 129 | } 130 | this.returnType = [type]; 131 | returns.push({ 132 | description: res.description, 133 | type: [type], 134 | arg: 'data', 135 | root: true, 136 | }); 137 | } else if (res.schema && res.schema.type === 'object') { 138 | type = (this.name + '_response_' + code).replace(/\./g, '_'); 139 | schema = {}; 140 | schema[type] = res.schema; 141 | models = buildModels(schema); 142 | this.models[type] = models[type]; 143 | this.returnType = type; 144 | returns.push({ 145 | description: res.description, 146 | type: type, 147 | arg: 'data', 148 | root: true, 149 | }); 150 | } 151 | } else { 152 | this.errorTypes.push({ 153 | statusCode: code, 154 | message: res.description, 155 | }); 156 | } 157 | } 158 | this.returns = returns; 159 | return this.returns; 160 | }; 161 | 162 | var VERBS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; 163 | 164 | /** 165 | * Derive the prefix from the base path 166 | * @param spec {Object} The Swagger spec object 167 | * @returns {String} Path prefix 168 | */ 169 | function getPathPrefix(spec) { 170 | var basePath = spec.basePath; 171 | if (!basePath) return ''; 172 | var prefix = basePath.split('/').filter(function(p) { 173 | return p !== '' && p !== '*'; 174 | }).slice(1).join('/'); // The first part will be ignored 175 | if (!prefix) return ''; 176 | else return '/' + prefix; 177 | } 178 | 179 | /** 180 | * Build a map of operations for the given Swagger spec 181 | * @param spec {Object} The Swagger spec object 182 | * @returns {Object} A map of operations keyed by path 183 | */ 184 | V2Generator.prototype.getOperations = function(spec) { 185 | assert(spec && spec.swagger === '2.0'); 186 | spec.definitions = spec.definitions || {}; 187 | // var info = spec.info; 188 | var prefix = getPathPrefix(spec); 189 | var models = spec.definitions; 190 | 191 | var operations = {}; 192 | var templates = spec['x-implementation-templates'] || {}; 193 | 194 | for (var path in spec.paths) { 195 | if (path.indexOf('x-') === 0) continue; 196 | var ops = spec.paths[path]; 197 | /* eslint-disable one-var */ 198 | for (var verb in ops) { 199 | // Skip non-verbs such as parameters or x-, $ref 200 | if (VERBS.indexOf(verb.toLowerCase()) === -1) continue; 201 | var op = ops[verb]; 202 | 203 | if (!op.parameters) { 204 | op.parameters = []; 205 | } 206 | 207 | op.tags = op.tags || []; 208 | 209 | op.models = models; 210 | 211 | op.verb = verb.toLowerCase(); 212 | // Replace {id} with :id 213 | op.path = path.replace(/{(([^{}])+)}/g, ':$1'); 214 | 215 | // operationId is optional 216 | if (!op.operationId) { 217 | // Derive the operationId from verb & path 218 | op.operationId = op.verb.toLowerCase() + '_' + op.path; 219 | } 220 | 221 | // Camelize the operation id 222 | op.operationId = op.operationId.replace(/{(([^{}])+)}/g, '_$1'); 223 | 224 | // Capture the short name 225 | op.name = op['x-operation-name'] || op.operationId; 226 | 227 | if (op.tags.length === 1) { 228 | var tag = op.tags[0].toLowerCase(); 229 | // Remove prefix that matches the tag name 230 | if (op.name.toLowerCase().indexOf(tag) === 0) { 231 | op.name = op.name.substring(tag.length); 232 | } 233 | if (op.path.toLowerCase() === ('/' + tag) || 234 | op.path.toLowerCase().indexOf('/' + tag + '/') === 0) { 235 | // Remove prefix for the path 236 | op.path = op.path.substring(tag.length + 1); 237 | } 238 | } 239 | 240 | var index = op.name.indexOf('.'); 241 | if (index !== -1) { 242 | op.name = op.name.substring(index + 1); 243 | if (op.name.indexOf('prototype.') === 0) { 244 | op.name = 'prototype.' + _.camelCase(op.name.substring(10)); 245 | } else { 246 | op.name = _.camelCase(op.name); 247 | } 248 | } else { 249 | op.name = _.camelCase(op.name); 250 | } 251 | 252 | op.operationId = _.camelCase(op.operationId); 253 | var implTemplate = op['x-implementation'] || 254 | op['x-implementation-template']; 255 | 256 | if (implTemplate) { 257 | var template = implTemplate.template; 258 | if (template && getRef(template) && 259 | getRef(template).indexOf('#/x-implementation-templates/') === 0) { 260 | // The template is a ref to the global templates 261 | var templateName = 262 | getRef(template).substring('#/x-implementation-templates/'.length); 263 | if (templates[templateName]) { 264 | var templateStr = templates[templateName]; 265 | if (templateStr.loopback) { 266 | // If there is a specific template for LoopBack, use it 267 | templateStr = templateStr.loopback; 268 | } 269 | if (Array.isArray(templateStr)) { 270 | // Allow the template to be string[] 271 | templateStr = templateStr.join('\n'); 272 | } 273 | if (typeof templateStr === 'string') { 274 | var compiled = _.template(templateStr); 275 | op.implementation = compiled(implTemplate.parameters || {}); 276 | } 277 | } 278 | } else { 279 | if (implTemplate.loopback) { 280 | // If there is a specific template for LoopBack, use it 281 | implTemplate = implTemplate.loopback; 282 | } 283 | // The template is code, either as string[] or string 284 | if (Array.isArray(implTemplate)) { 285 | implTemplate = implTemplate.join('\n'); 286 | op.implementation = implTemplate; 287 | } else if (typeof implTemplate === 'string') { 288 | op.implementation = implTemplate; 289 | } 290 | } 291 | } 292 | 293 | var operation = new V2Operation(op); 294 | operation.getRemoting(); 295 | 296 | // Append the prefix 297 | operation.path = prefix + operation.path; 298 | 299 | operations[operation.path] = operations[operation.path] || {}; 300 | operations[operation.path][operation.verb] = operation; 301 | } 302 | /* eslint-enable one-var */ 303 | } 304 | return operations; 305 | }; 306 | 307 | function getRef(obj) { 308 | return (obj != null && typeof obj === 'object') && 309 | (obj.$ref || obj.$REF); 310 | } 311 | 312 | module.exports = V2Generator; 313 | 314 | -------------------------------------------------------------------------------- /lib/codegen/json-schema.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | function getRef(obj) { 9 | return (obj != null && typeof obj === 'object') && 10 | (obj.$ref || obj.$REF); 11 | } 12 | 13 | function resolveTypeRef(schema, ref) { 14 | if (typeof ref === 'string') { 15 | if (ref.indexOf('#/definitions/') === 0) { 16 | ref = ref.substring('#/definitions/'.length); 17 | } 18 | if (ref.indexOf('#/models/') === 0) { 19 | ref = ref.substring('#/models/'.length); 20 | } 21 | var model = schema[ref]; 22 | if (model) { 23 | if (model.type === 'object' || !model.type) { 24 | return ref; 25 | } 26 | if (model.type === 'array') { 27 | var itemType = model.items.type; 28 | if (getRef(model.items)) { 29 | itemType = resolveTypeRef(schema, getRef(model.items)); 30 | } 31 | return [itemType]; 32 | } else { 33 | return model.type; 34 | } 35 | } 36 | } 37 | return ref; 38 | } 39 | 40 | /** 41 | * Build a LoopBack property definition from the JSON model 42 | * @param {Object} schema JSON Schema 43 | * @param {Object} jsonModel The json model definition 44 | * @param {String} propertyName The property name 45 | * @returns {Object} 46 | */ 47 | function buildProperty(schema, jsonModel, propertyName) { 48 | var jsonProperty = jsonModel.properties[propertyName]; 49 | var property = {}; 50 | 51 | var type = jsonProperty.type; 52 | if (getRef(jsonProperty)) { 53 | type = resolveTypeRef(schema, getRef(jsonProperty)); 54 | } 55 | 56 | if (type === 'array' && jsonProperty.items) { 57 | var itemType = jsonProperty.items.type; 58 | if (getRef(jsonProperty.items)) { 59 | itemType = resolveTypeRef(schema, getRef(jsonProperty.items)); 60 | } 61 | type = [itemType]; 62 | } 63 | if (type === 'integer') { 64 | type = 'number'; 65 | } 66 | property.type = type; 67 | if (Array.isArray(jsonModel.required) && 68 | jsonModel.required.indexOf(propertyName) !== -1) { 69 | property.required = true; 70 | } 71 | for (var a in jsonProperty) { 72 | if (a === '$ref' || a === '$REF' || a === 'items' || (a in property)) { 73 | continue; 74 | } 75 | property[a] = jsonProperty[a]; 76 | } 77 | return property; 78 | } 79 | 80 | function buildModel(models, schema, jsonModel, modelName, anonymous) { 81 | if (models[modelName]) { 82 | return models[modelName]; 83 | } 84 | if (jsonModel.type && jsonModel.type !== 'object') { 85 | // The model is either an array or primitive type 86 | return; 87 | } 88 | var model = {name: modelName, properties: {}}; 89 | var base, prop; 90 | // Handle allOf 91 | if (Array.isArray(jsonModel.allOf)) { 92 | var refs = []; 93 | var required = []; 94 | /* eslint-disable one-var */ 95 | for (var i = 0, n = jsonModel.allOf.length; i < n; i++) { 96 | var item = jsonModel.allOf[i]; 97 | 98 | if (Array.isArray(item.required)) { 99 | required = required.concat(item.required); 100 | } 101 | var itemModel; 102 | if (getRef(item)) { 103 | // Extract model name from reference object 104 | base = models[getRef(item).substring('#/definitions/'.length)] || base; 105 | refs.push(base); 106 | } else { 107 | // Build the embedded model 108 | itemModel = buildModel(models, schema, item, modelName + '_' + i, true); 109 | } 110 | if (itemModel) { 111 | // Add more item model properties to the model 112 | for (prop in itemModel.properties) { 113 | model.properties[prop] = itemModel.properties[prop]; 114 | } 115 | } 116 | } 117 | /* eslint-enable one-var */ 118 | if (refs.length === 1) { 119 | // Set the referenced model as the base 120 | model.base = (base && base.name) || base; 121 | } else { 122 | // Mix in all properties from the referenced models 123 | for (i = 0, n = refs.length; i < n; i++) { 124 | // Add more item model properties to the model 125 | for (prop in refs[i].properties) { 126 | model.properties[prop] = refs[i].properties[prop]; 127 | } 128 | } 129 | } 130 | for (prop in model.properties) { 131 | if (required.indexOf(prop) !== -1) { 132 | model.properties[prop].required = true; 133 | } 134 | } 135 | } 136 | if (jsonModel['x-base-type']) { 137 | model.base = jsonModel['x-base-type']; 138 | } 139 | /* eslint-disable one-var */ 140 | for (var p in jsonModel.properties) { 141 | var property = buildProperty(schema, jsonModel, p); 142 | model.properties[p] = property; 143 | } 144 | 145 | if (typeof jsonModel['x-relations'] === 'object') { 146 | model.relations = {}; 147 | for (var r in jsonModel['x-relations']) { 148 | var rel = jsonModel['x-relations'][r]; 149 | if (rel.partner && getRef(rel.partner)) { 150 | rel.model = resolveTypeRef(schema, getRef(rel.partner)); 151 | delete rel.partner; 152 | } 153 | model.relations[r] = rel; 154 | } 155 | } 156 | 157 | /* eslint-enable one-var */ 158 | if (!anonymous) { 159 | models[modelName] = model; 160 | } 161 | return model; 162 | } 163 | 164 | /** 165 | * Convert the JSON-schema to LoopBack model definitions 166 | * @param {Object} schema 167 | */ 168 | module.exports = function(schema) { 169 | var models = {}; 170 | if (!schema) { 171 | return models; 172 | } 173 | for (var m in schema) { 174 | var jsonModel = schema[m]; 175 | buildModel(models, schema, jsonModel, m); 176 | } 177 | return models; 178 | }; 179 | -------------------------------------------------------------------------------- /lib/codegen/model-template.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var fs = require('fs'); 9 | var path = require('path'); 10 | 11 | var template = fs.readFileSync(path.join(__dirname, 'model.ejs'), 'UTF-8'); 12 | module.exports = template; 13 | -------------------------------------------------------------------------------- /lib/codegen/model.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | function normalizeName(name) { 3 | return name.replace(/[\-\.\s]/g, '_'); 4 | } 5 | 6 | function printParams() { 7 | var params = []; 8 | for (var j = 0; j < op.accepts.length; j++) { 9 | var param = op.accepts[j]; 10 | var type = param.type || '*'; 11 | if (Array.isArray(param.type)) { 12 | type = param.type[0] + '[]'; 13 | } 14 | params.push(' * @param {' + type + '} ' + normalizeName(param.arg) + 15 | ' ' + param.description); 16 | } 17 | return params.join('\n'); 18 | } 19 | 20 | 21 | function printMethod(modelName) { 22 | var className = modelName || 'Model'; 23 | var method = className + '.' + op.operationId 24 | + ' = function('; 25 | var params = op.accepts.map(function(a) { 26 | return normalizeName(a.arg); 27 | }); 28 | params.push('callback'); 29 | return method + params.join(', ') + ') {'; 30 | } 31 | 32 | %> 33 | module.exports = function(<%- modelName || 'Model' %>) { 34 | <% 35 | var i; 36 | for(var i = 0; i < operations.length; i++) { 37 | var op = operations[i]; 38 | %> 39 | /** 40 | * <%- op.description || op.summary || op.operationId %> 41 | <%- printParams() %> 42 | * @callback {Function} callback Callback function 43 | * @param {Error|string} err Error object 44 | * @param {<%- op.returnType %>} result Result object 45 | */ 46 | <%- printMethod(modelName) %> 47 | <% if (op.implementation) { %> 48 | <%- op.implementation %> 49 | } 50 | <% } else { %> 51 | // Replace the code below with your implementation. 52 | // Please make sure the callback is invoked. 53 | process.nextTick(function() { 54 | var err = new Error('Not implemented'); 55 | callback(err); 56 | }); 57 | <% for(var k = 0; k < op.errorTypes.length; k++) { var et = op.errorTypes[k]; %> 58 | /* 59 | var err<%- k %> = new Error('<%- et.message %>'); 60 | err<%- k %>.statusCode = <%- et.statusCode %>; 61 | return cb(err<%- k %>); 62 | */ <% } %> 63 | } 64 | <% } %> 65 | <% } %> 66 | 67 | <% 68 | for(i = 0; i < operations.length; i++) { 69 | var op = operations[i]; 70 | %> 71 | <%- modelName || 'Model' %>.remoteMethod('<%- op.operationId %>', 72 | <%- op.printRemoting() %> 73 | ); 74 | <% } %> 75 | } 76 | -------------------------------------------------------------------------------- /lib/codegen/spec-converter.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var url = require('url'); 10 | 11 | function assign(target, targetProperty, source, sourceProperty, defaultValue) { 12 | if (source && source[sourceProperty]) { 13 | target[targetProperty] = source[sourceProperty] || defaultValue; 14 | } else if (defaultValue) { 15 | target[targetProperty] = defaultValue; 16 | } 17 | } 18 | 19 | var primitiveTypes = ['integer', 'number', 'string', 'boolean', 'file']; 20 | 21 | module.exports.sourceVersion = 'Swagger 1.2'; 22 | 23 | module.exports.applies = function(data) { 24 | return (data.swaggerVersion && data.swaggerVersion === '1.2'); 25 | }; 26 | 27 | function convertApi(v2, apiDeclaration, definitions) { 28 | if (v2.info.version == null) { 29 | v2.info.version = apiDeclaration.apiVersion; 30 | } 31 | 32 | if (v2.host === 'unknown' || v2.basePath === 'unknown') { 33 | var base = url.parse(apiDeclaration.basePath); 34 | v2.host = base.host; 35 | v2.basePath = base.pathname; 36 | 37 | // This assumes that the the schemes in the apiDeclaration basePath are the only ones supported. 38 | // 39 | if (!_.contains(v2.schemes, base.scheme)) { 40 | v2.schemes.push(base.protocol.replace(':', '')); 41 | } 42 | } 43 | 44 | var resourceProduces = apiDeclaration.produces; 45 | var resourceConsumes = apiDeclaration.consumes; 46 | 47 | // What to do with authorizations -> security? 48 | // Assuming security is an array of permissions/oauth scopes 49 | // 50 | // Pull apiDeclaration level security down to operations 51 | // 52 | var resourceSecurity = null; 53 | 54 | if (apiDeclaration.authorizations && 55 | apiDeclaration.authorizations.oauth && 56 | apiDeclaration.authorizations.oauth.scopes) { 57 | resourceSecurity = _.map(apiDeclaration.authorizations.oauth.scopes, function(scope) { 58 | return scope.scope; 59 | }); 60 | } 61 | 62 | _.each(apiDeclaration.apis, function(api) { 63 | var path = v2.paths[api.path] = {}; 64 | 65 | _.each(api.operations, function(operation) { 66 | var method = {}; 67 | 68 | assign(method, 'summary', operation, 'summary'); 69 | assign(method, 'description', operation, 'notes'); 70 | assign(method, 'operationId', operation, 'nickname'); 71 | 72 | if (resourceProduces) { 73 | method.produces = resourceProduces; 74 | } 75 | 76 | assign(method, 'produces', operation, 'produces'); 77 | 78 | if (resourceConsumes) { 79 | method.consumes = resourceConsumes; 80 | } 81 | 82 | assign(method, 'consumes', operation, 'consumes'); 83 | 84 | if (operation.parameters && operation.parameters.length > 0) { 85 | method.parameters = _.map(operation.parameters, function(parameter) { 86 | var converted = { 87 | name: parameter.name, 88 | in: parameter.paramType === 'form' ? 'formData' : parameter.paramType, 89 | }; 90 | 91 | if (parameter.description) { 92 | // TODO: [rfeng] Convert html to md 93 | // converted.description = md(parameter.description); 94 | converted.description = parameter.description; 95 | } 96 | 97 | converted.required = parameter.paramType === 'body' ? true : parameter.required; 98 | assign(converted, 'uniqueItems', parameter, 'uniqueItems'); 99 | 100 | var target = converted; 101 | var props = {}; 102 | 103 | if (parameter.paramType === 'body' && parameter.type === 'array') { 104 | // This would not validate through the schema. Issue? 105 | // 106 | // props.type = "array"; 107 | // props.items = { 108 | props.schema = { 109 | '$ref': '#/definitions/' + parameter.items['$ref'], 110 | }; 111 | } else if (_.contains(primitiveTypes, parameter.type)) { 112 | props.type = parameter.type; 113 | 114 | if (parameter.format) { 115 | props.format = parameter.format; 116 | } 117 | } else { 118 | props.schema = { 119 | '$ref': '#/definitions/' + parameter.type, 120 | }; 121 | } 122 | /* 123 | // Do not apply to parameters. It seems to be in 1.2 by way of dataTypeBase.json. Issue? 124 | // Commented out to pass validation. 125 | // 126 | assign(props, 'default', parameter, 'defaultValue'); 127 | assign(props, 'enum', parameter, 'enum'); 128 | 129 | if (parameter.type === "integer") { 130 | if (parameter.minimum) props.minimum = parseInt(parameter.minimum); 131 | if (parameter.maximum) props.maximum = parseInt(parameter.maximum); 132 | } else if (parameter.type === "number") { 133 | if (parameter.minimum) props.minimum = parseFloat(parameter.minimum); 134 | if (parameter.maximum) props.maximum = parseFloat(parameter.maximum); 135 | } 136 | */ 137 | 138 | if (parameter.allowMultiple) { 139 | target.type = 'array'; 140 | target.items = props; 141 | } else { 142 | _.extend(target, props); 143 | } 144 | 145 | assign(converted, 'x-typeArguments', parameter, 'typeArguments'); 146 | 147 | return converted; 148 | }); 149 | } 150 | 151 | var responses = {}; 152 | 153 | if ( 154 | operation.type && 155 | 156 | operation.type !== 'void') { 157 | var items = {}; 158 | var response = { 159 | description: 'Success', 160 | schema: {}, 161 | }; 162 | 163 | if (operation.type === 'array') { 164 | response.schema.type = 165 | 'array'; 166 | response.schema.items = { 167 | '$ref': '#/definitions/' + operation.items['$ref'], 168 | }; 169 | } else if (_.contains(primitiveTypes, operation.type)) { 170 | response.schema.type = operation.type; 171 | 172 | if (operation.format) { 173 | response.schema.format = operation.format; 174 | } 175 | } else { 176 | response.schema['$ref'] = '#/definitions/' + operation.type; 177 | } 178 | responses['200'] = response; 179 | } 180 | 181 | if (operation.responseMessages) { 182 | _.each(operation.responseMessages, function(response) { 183 | responses[response.code] = { 184 | description: response.message, 185 | }; 186 | }); 187 | } 188 | if (_.isEmpty(responses)) { 189 | responses['200'] = {description: 'Success'}; 190 | } 191 | 192 | method.responses = responses; 193 | 194 | if (resourceSecurity) { 195 | method.security = resourceSecurity; 196 | } 197 | 198 | // TODO schemes 199 | // What to do with authorizations -> security? 200 | // Assuming security is an array of permissions/oauth scopes 201 | // 202 | if (operation.authorizations && operation.authorizations.oauth && 203 | operation.authorizations.oauth.scopes) { 204 | method.security = _.map(operation.authorizations.oauth.scopes, 205 | function(scope) { 206 | return scope.scope; 207 | }); 208 | } 209 | 210 | path[operation.method.toLowerCase()] = method; 211 | }); 212 | }); 213 | 214 | _.each(apiDeclaration.models, function(model, name) { 215 | var definition = {}; 216 | 217 | assign(definition, 'required', model, 'required'); 218 | definition.properties = {}; 219 | _.each(model.properties, function(property, name) { 220 | var converted = _.clone(property); 221 | 222 | assign(converted, 'type', property, 'type'); 223 | assign(converted, 'schema', property, 'schema'); 224 | assign(converted, 'items', property, 'items'); 225 | assign(converted, 'default', property, 'defaultValue'); 226 | assign(converted, 'enum', property, 'enum'); 227 | 228 | if (property.type === 'integer') { 229 | if (property.minimum) { 230 | converted.minimum = parseInt(property.minimum); 231 | } 232 | if (property.maximum) { 233 | converted.maximum = parseInt(property.maximum); 234 | } 235 | } else if (converted.type === 'number') { 236 | if (property.minimum) { 237 | converted.minimum = parseFloat(property.minimum); 238 | } 239 | if (property.maximum) { 240 | converted.maximum = parseFloat(property.maximum); 241 | } 242 | } 243 | 244 | definition.properties[name] = converted; 245 | }); 246 | 247 | // TODO 248 | // subTypes 249 | // discriminator 250 | // 251 | 252 | assign(definition, 'x-typeParameters', model, 'typeParameters'); 253 | 254 | definitions[name] = definition; 255 | }); 256 | } 257 | 258 | module.exports.convert = function(specUrl, resourceListing, apiDeclarations) { 259 | var v2 = { 260 | swagger: '2.0', 261 | }; 262 | 263 | if (resourceListing.info) { 264 | v2.info = {}; 265 | v2.info.title = resourceListing.info.title; 266 | v2.info.version = resourceListing.apiVersion; 267 | v2.info.description = resourceListing.info.description; 268 | // v2.info.description = md(resourceListing.info.description); 269 | assign(v2.info, 'termsOfService', resourceListing.info, 'termsOfServiceUrl'); 270 | 271 | if (resourceListing.info.contact) { 272 | v2.info.contact = {name: resourceListing.info.contact}; 273 | } 274 | 275 | if (resourceListing.info.license) { 276 | v2.info.license = v2.info.license || {}; 277 | v2.info.license.name = resourceListing.info.license; 278 | } 279 | 280 | if (resourceListing.info.licenseUrl) { 281 | v2.info.license = v2.info.license || {}; 282 | v2.info.license.url = resourceListing.info.licenseUrl; 283 | } 284 | 285 | // TODO authorizations? 286 | } 287 | 288 | v2.host = 'unknown'; 289 | v2.basePath = 'unknown'; 290 | v2.schemes = []; 291 | 292 | v2['x-resources'] = resourceListing.apis; 293 | v2.paths = {}; 294 | 295 | var definitions = {}; 296 | 297 | _.each(apiDeclarations, function(apiDeclaration) { 298 | convertApi(v2, apiDeclaration, definitions); 299 | }); 300 | }; 301 | -------------------------------------------------------------------------------- /lib/specgen/model-helper.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | var schemaBuilder = require('./schema-builder'); 12 | var typeConverter = require('./type-converter'); 13 | var TypeRegistry = require('./type-registry'); 14 | var _ = require('lodash'); 15 | 16 | /** 17 | * Export the modelHelper singleton. 18 | */ 19 | var modelHelper = module.exports = { 20 | /** 21 | * Given a class (from remotes.classes()), generate a model definition. 22 | * This is used to generate the schema at the top of many endpoints. 23 | * @param {Class} modelClass Model class. 24 | * @param {TypeRegistry} typeRegistry Registry of types and models. 25 | * @return {Object} Associated model definition. 26 | */ 27 | registerModelDefinition: function(modelCtor, typeRegistry, opts) { 28 | var lbdef = modelCtor.definition; 29 | 30 | if (!lbdef) { 31 | // The model does not have any definition, it was most likely 32 | // created as a placeholder for an unknown property type 33 | return; 34 | } 35 | 36 | var name = lbdef.name; 37 | if (typeRegistry.isDefined(name)) { 38 | // The model is already included 39 | return; 40 | } 41 | 42 | typeRegistry.registerModel(name, function() { 43 | return definitionFunction(modelCtor, typeRegistry); 44 | }); 45 | 46 | // check if ModelClass has defined getUpdateOnlyProperties() function to 47 | // avoid version issues 48 | if (opts && opts.generateOperationScopedModels && modelCtor.getUpdateOnlyProperties) { 49 | const excludeProps = modelCtor.getUpdateOnlyProperties(); 50 | // if at least one excludeProp is found, we need to create another model 51 | // to be used only for create operation and this model will not have 52 | // excludeProp included in the model. e.g generated "id" property 53 | if (excludeProps && excludeProps.length > 0) { 54 | const modelName = '$new_' + name; 55 | typeRegistry.registerModel(modelName, function() { 56 | return definitionFunction(modelCtor, typeRegistry, {excludeProps}); 57 | }); 58 | } 59 | } 60 | 61 | if (opts && opts.generateRelationProperties) { 62 | const modelNameWithRelations = name + 'WithRelations'; 63 | typeRegistry.registerModel(modelNameWithRelations, function() { 64 | return definitionFunction(modelCtor, typeRegistry, {includeRelations: true}); 65 | }); 66 | } 67 | }, 68 | 69 | isHiddenProperty: function(definition, propName) { 70 | return definition.settings && 71 | Array.isArray(definition.settings.hidden) && 72 | definition.settings.hidden.indexOf(propName) !== -1; 73 | }, 74 | }; 75 | 76 | var definitionFunction = function(modelCtor, typeRegistry, options) { 77 | var lbdef = modelCtor.definition; 78 | 79 | var swaggerDef = { 80 | description: typeConverter.convertText( 81 | lbdef.description || (lbdef.settings && lbdef.settings.description) 82 | ), 83 | properties: {}, 84 | required: [], 85 | }; 86 | 87 | if (lbdef.settings && lbdef.settings.swagger && lbdef.settings.swagger.example) { 88 | swaggerDef.example = lbdef.settings.swagger.example; 89 | } 90 | 91 | addSwaggerExtensions(lbdef.settings); 92 | 93 | var properties = lbdef.rawProperties || lbdef.properties; 94 | 95 | // Iterate through each property in the model definition. 96 | // Types may be defined as constructors (e.g. String, Date, etc.), 97 | // or as strings; swaggerSchema.buildFromLoopBackType() will take 98 | // care of the conversion. 99 | Object.keys(properties).forEach(function(key) { 100 | var prop = properties[key]; 101 | 102 | // Hide hidden properties. 103 | if (modelHelper.isHiddenProperty(lbdef, key)) 104 | return; 105 | 106 | // Get a type out of the constructors we were passed. 107 | var schema = schemaBuilder.buildFromLoopBackType(prop, typeRegistry); 108 | 109 | var desc = typeConverter.convertText(prop.description || prop.doc); 110 | if (desc) schema.description = desc; 111 | 112 | // Required props sit in a per-model array. 113 | if (prop.required || (prop.id && !prop.generated)) { 114 | swaggerDef.required.push(key); 115 | } 116 | 117 | // if model has excludeProps properties, skip adding to the model since this 118 | // model is used for create operation only where this property 119 | // should not exist 120 | if (options && options.excludeProps && _.includes(options.excludeProps, key)) { 121 | return; 122 | } 123 | swaggerDef.properties[key] = schema; 124 | }); 125 | 126 | if (lbdef.settings) { 127 | var strict = lbdef.settings.strict; 128 | var additionalProperties = lbdef.settings.additionalProperties; 129 | var notAllowAdditionalProperties = strict || (additionalProperties !== true); 130 | if (notAllowAdditionalProperties) { 131 | swaggerDef.additionalProperties = false; 132 | } 133 | } 134 | 135 | if (!swaggerDef.required.length) { 136 | // "required" must have at least one item when present 137 | delete swaggerDef.required; 138 | } 139 | 140 | // Add models from settings 141 | if (lbdef.settings && lbdef.settings.models) { 142 | for (var m in lbdef.settings.models) { 143 | var model = modelCtor[m]; 144 | if (typeof model !== 'function' || !model.modelName) continue; 145 | modelHelper.registerModelDefinition(model, typeRegistry); 146 | typeRegistry.reference(model.modelName); 147 | } 148 | } 149 | 150 | // Generate model definitions for related models 151 | /* eslint-disable one-var */ 152 | for (var r in modelCtor.relations) { 153 | var rel = modelCtor.relations[r]; 154 | if (rel.modelTo) { 155 | modelHelper.registerModelDefinition(rel.modelTo, typeRegistry); 156 | } 157 | if (rel.modelThrough) { 158 | modelHelper.registerModelDefinition(rel.modelThrough, typeRegistry); 159 | } 160 | 161 | var prop = null; 162 | var relationOptions = rel.options || {}; 163 | 164 | if (options && !!options.includeRelations) { 165 | if (!relationOptions.disableInclude) { 166 | const modelNameWithRelations = rel.modelTo.modelName + 'WithRelations'; 167 | // now we make it look like a loopback property definition 168 | switch (rel.type) { 169 | case 'hasMany': 170 | case 'hasManyThrough': 171 | case 'hasAndBelongsToMany': 172 | case 'embedsMany': 173 | case 'referencesMany': 174 | prop = [modelNameWithRelations]; 175 | break; 176 | case 'hasOne': 177 | case 'belongsTo': 178 | case 'embedsOne': 179 | prop = modelNameWithRelations; 180 | break; 181 | default: 182 | // there above were all relation types covered in the docs 183 | // so we shouldn't be here -- log some error? 184 | } 185 | 186 | if (prop) { 187 | // Get a type out of the constructors we were passed. 188 | var schema = schemaBuilder.buildFromLoopBackType(prop, typeRegistry, options); 189 | 190 | // Assign the schema to the properties object. 191 | swaggerDef.properties[r] = schema; 192 | } 193 | } 194 | } 195 | } 196 | /* eslint-enable one-var */ 197 | return swaggerDef; 198 | 199 | function addSwaggerExtensions(defs) { 200 | for (var def in defs) { 201 | if (def.match(/^x\-/)) { 202 | swaggerDef[def] = defs[def]; 203 | } 204 | } 205 | }; 206 | }; 207 | 208 | -------------------------------------------------------------------------------- /lib/specgen/route-helper.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // Globalization 9 | var g = require('strong-globalize')(); 10 | 11 | /** 12 | * Module dependencies. 13 | */ 14 | 15 | var debug = require('debug')('loopback:explorer:routeHelpers'); 16 | var _assign = require('lodash').assign; 17 | var typeConverter = require('./type-converter'); 18 | var schemaBuilder = require('./schema-builder'); 19 | 20 | /** 21 | * Export the routeHelper singleton. 22 | */ 23 | var routeHelper = module.exports = { 24 | /** 25 | * Given a route, generate an API description and add it to the doc. 26 | * If a route shares a path with another route (same path, different verb), 27 | * add it as a new operation under that path entry. 28 | * 29 | * Routes can be translated to API declaration 'operations', 30 | * but they need a little massaging first. The `accepts` and 31 | * `returns` declarations need some basic conversions to be compatible. 32 | * 33 | * This method will convert the route and add it to the doc. 34 | * 35 | * @param {Route} route Strong Remoting Route object. 36 | * @param {Class} classDef Strong Remoting class. 37 | * @param {TypeRegistry} typeRegistry Registry of types and models. 38 | * @param {Object} operationIdRegistry Registry of operationIds mapping 39 | * operationId to an operation object. 40 | * @param {Object} paths Swagger Path Object, 41 | * see https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#pathsObject 42 | */ 43 | addRouteToSwaggerPaths: function(route, classDef, typeRegistry, 44 | operationIdRegistry, paths, opts) { 45 | var entryToAdd = routeHelper.routeToPathEntry(route, classDef, 46 | typeRegistry, 47 | operationIdRegistry, opts); 48 | if (!(entryToAdd.path in paths)) { 49 | paths[entryToAdd.path] = {}; 50 | } 51 | paths[entryToAdd.path][entryToAdd.method] = entryToAdd.operation; 52 | }, 53 | 54 | /** 55 | * Massage route.accepts. 56 | * @param {Object} route Strong Remoting Route object. 57 | * @param {Class} classDef Strong Remoting class. 58 | * @param {TypeRegistry} typeRegistry Registry of types and models. 59 | * @return {Array} Array of param docs. 60 | */ 61 | convertAcceptsToSwagger: function(route, classDef, typeRegistry, opts) { 62 | var accepts = route.accepts || []; 63 | var split = route.method.split('.'); 64 | if (classDef && classDef.sharedCtor && 65 | classDef.sharedCtor.accepts && split.length > 2 /* HACK */) { 66 | accepts = [].concat(classDef.sharedCtor.accepts) 67 | .concat(accepts); 68 | } 69 | 70 | // Filter out parameters that are generated from the incoming request, 71 | // or generated by functions that use those resources. 72 | accepts = accepts.filter(function(arg) { 73 | // Allow undocumenting a param. 74 | if (arg.documented === false) return false; 75 | // Below conditions are only for 'http' 76 | if (!arg.http) return true; 77 | // Don't show derived arguments. 78 | if (typeof arg.http === 'function') return false; 79 | // Don't show arguments set to the incoming http request. 80 | // Please note that body needs to be shown, such as User.create(). 81 | if (arg.http.source === 'req' || 82 | arg.http.source === 'res' || 83 | arg.http.source === 'context') { 84 | return false; 85 | } 86 | return true; 87 | }); 88 | 89 | // Turn accept definitions in to parameter docs. 90 | accepts = accepts.map( 91 | routeHelper.acceptToParameter(route, classDef, typeRegistry, opts) 92 | ); 93 | 94 | return accepts; 95 | }, 96 | 97 | /** 98 | * Massage route.returns. 99 | * @param {Object} route Strong Remoting Route object. 100 | * @return {Object} A single returns param doc. 101 | */ 102 | convertReturnsToSwagger: function(route, typeRegistry, opts) { 103 | if (opts && opts.generateRelationProperties) { 104 | if (route.method.match(/\.(find|findOne|findById|__(get|findById)__*)/)) { 105 | var returns = route.returns; 106 | var type = returns[0].type; 107 | var newType = type + 'WithRelations'; 108 | returns[0].type = Array.isArray(type) ? [newType] : newType; 109 | route.returns = returns; 110 | } 111 | } 112 | 113 | var routeReturns = route.returns; 114 | if (!routeReturns || !routeReturns.length) { 115 | // An operation that returns nothing will have 116 | // no schema declaration for its response. 117 | return undefined; 118 | } 119 | 120 | // Filter out arguments having http.target set to 'header' or 'status' 121 | routeReturns = routeReturns.filter(function(arg) { 122 | const target = arg.http && arg.http.target; 123 | return target !== 'header' && target !== 'status'; 124 | }); 125 | 126 | if (routeReturns.length === 1 && routeReturns[0].root) { 127 | if (routeReturns[0].model) { 128 | return {$ref: typeRegistry.reference(routeReturns[0].model)}; 129 | } 130 | return schemaBuilder.buildFromLoopBackType(routeReturns[0], typeRegistry); 131 | } else if (routeReturns.length === 1 && routeReturns[0].type === 'ReadableStream') { 132 | return {type: 'file'}; 133 | } 134 | 135 | // Construct scheme for the return object 136 | var schema = {type: 'object'}; 137 | schema.properties = {}; 138 | routeReturns.forEach(function(ret) { 139 | var propName = ret.name || ret.arg; 140 | var idlType = schemaBuilder.getLdlTypeName(ret.type); 141 | // Take care of array which can be nested. 142 | // Note that we cannot simply use buildFromLoopBackType since it converts unknown type to 143 | // '$ref': '#/definitions/UnknownType', whereas we decided to emit 'type: object' for such a case. 144 | // See https://github.com/strongloop/loopback-swagger/pull/28#discussion_r54873911 145 | // The following code is needed to take care of a nested array of an unknown type. 146 | var itemIdlType = idlType; 147 | var genericIdlType = {type: 'object'}; 148 | while (Array.isArray(itemIdlType)) { 149 | itemIdlType = schemaBuilder.getLdlTypeName(itemIdlType[0]); 150 | genericIdlType = {type: 'array', items: genericIdlType}; 151 | } 152 | if (schemaBuilder.isPrimitiveType(itemIdlType)) { 153 | schema.properties[propName] = 154 | schemaBuilder.buildFromLoopBackType(ret, typeRegistry); 155 | } else { 156 | if (typeRegistry.isDefined(itemIdlType)) { 157 | schema.properties[propName] = 158 | schemaBuilder.buildFromLoopBackType(ret, typeRegistry); 159 | } else { 160 | debug('Swagger: temporarily using `object` instead of unknown ' + 161 | 'type %j found in route %j', itemIdlType, route); 162 | schema.properties[propName] = genericIdlType; 163 | } 164 | } 165 | if (ret.required) { 166 | if (schema.required == null) { 167 | schema.required = []; 168 | } 169 | schema.required.push(propName); 170 | } 171 | }); 172 | 173 | return schema; 174 | }, 175 | 176 | /** 177 | * Converts from an sl-remoting-formatted "Route" description to a 178 | * Swagger-formatted "Path Item Object" 179 | * See swagger-spec/2.0.md#pathItemObject 180 | */ 181 | routeToPathEntry: function(route, classDef, 182 | typeRegistry, operationIdRegistry, opts) { 183 | // Some parameters need to be altered; eventually most of this should 184 | // be removed. 185 | var accepts = routeHelper.convertAcceptsToSwagger(route, classDef, 186 | typeRegistry, opts); 187 | var returns = routeHelper.convertReturnsToSwagger(route, typeRegistry, opts); 188 | var statusCode = route.returns && route.returns.length ? 200 : 204; 189 | 190 | if (route.http && route.http.status) { 191 | statusCode = route.http.status; 192 | } 193 | 194 | var responseMessages = {}; 195 | responseMessages[statusCode] = { 196 | description: 'Request was successful', 197 | schema: returns, 198 | // TODO - headers, examples 199 | }; 200 | 201 | if (route.returns && route.returns[0] && route.returns[0].example) { 202 | responseMessages[statusCode].examples = responseMessages[statusCode].examples || {}; 203 | responseMessages[statusCode].examples['application/json'] = route.returns[0].example; 204 | } 205 | 206 | if (route.errors) { 207 | // TODO define new LDL syntax that is status-code-indexed 208 | // and which allow users to specify headers & examples 209 | route.errors.forEach(function(msg) { 210 | var schema = null; 211 | if (msg.responseModel) { 212 | schema = schemaBuilder.buildFromLoopBackType(msg.responseModel, 213 | typeRegistry); 214 | } 215 | responseMessages[msg.code] = { 216 | description: msg.message, 217 | schema: schema, 218 | // TODO - headers, examples 219 | }; 220 | }); 221 | } 222 | 223 | if (route.http && route.http.errorStatus) { 224 | var errorStatus = route.http.errorStatus; 225 | if (!responseMessages[errorStatus]) { 226 | responseMessages[errorStatus] = { 227 | description: 'Unknown error', 228 | // TODO - headers, examples 229 | }; 230 | } 231 | } 232 | 233 | debug('route %j', route); 234 | 235 | var path = routeHelper.convertPathFragments(route.path); 236 | var verb = routeHelper.convertVerb(route.verb); 237 | 238 | var tags = []; 239 | var swaggerSettings = classDef && classDef.ctor && classDef.ctor.settings && 240 | classDef.ctor.settings.swagger || {}; 241 | 242 | if (swaggerSettings.tag && swaggerSettings.tag.name) { 243 | tags.push(swaggerSettings.tag.name); 244 | } else if (classDef && classDef.name) { 245 | tags.push(classDef.name); 246 | } 247 | 248 | var operationId = createUniqueOperationId(route.method, verb, path, 249 | operationIdRegistry); 250 | var entry = { 251 | path: path, 252 | method: verb, 253 | operation: { 254 | tags: tags, 255 | summary: typeConverter.convertText(route.description), 256 | description: typeConverter.convertText(route.notes), 257 | operationId: operationId, 258 | // [bajtos] we are omitting consumes and produces, as they are same 259 | // for all methods and they are already specified in top-level fields 260 | parameters: accepts, 261 | responses: responseMessages, 262 | deprecated: !!route.deprecated, 263 | // TODO: security 264 | }, 265 | }; 266 | 267 | operationIdRegistry[operationId] = entry; 268 | 269 | return entry; 270 | }, 271 | 272 | convertPathFragments: function convertPathFragments(path) { 273 | return path.split('/').map(function(fragment) { 274 | if (fragment.charAt(0) === ':') { 275 | return '{' + fragment.slice(1) + '}'; 276 | } 277 | return fragment; 278 | }).join('/'); 279 | }, 280 | 281 | convertVerb: function convertVerb(verb) { 282 | if (verb.toLowerCase() === 'all') { 283 | return 'post'; 284 | } 285 | 286 | if (verb.toLowerCase() === 'del') { 287 | return 'delete'; 288 | } 289 | 290 | return verb.toLowerCase(); 291 | }, 292 | 293 | /** 294 | * A generator to convert from an sl-remoting-formatted "Accepts" description 295 | * to a Swagger-formatted "Parameter" description. 296 | */ 297 | acceptToParameter: function acceptToParameter(route, classDef, typeRegistry, opts) { 298 | var DEFAULT_TYPE = 299 | route.verb.toLowerCase() === 'get' ? 'query' : 'formData'; 300 | 301 | return function(accepts) { 302 | var name = accepts.name || accepts.arg; 303 | var paramType = DEFAULT_TYPE; 304 | 305 | // TODO: Regex. This is leaky. 306 | if (route.path.indexOf(':' + name) !== -1) { 307 | paramType = 'path'; 308 | } 309 | 310 | // Check the http settings for the argument 311 | if (accepts.http && accepts.http.source) { 312 | paramType = accepts.http.source === 'form' ? 313 | 'formData' : 314 | accepts.http.source; 315 | } 316 | 317 | // TODO: ensure that paramType has a valid value 318 | // path, query, header, body, formData 319 | // See swagger-spec/2.0.md#parameterObject 320 | 321 | var paramObject = { 322 | name: name, 323 | in: paramType, 324 | description: typeConverter.convertText(accepts.description), 325 | // For path parameters, required must be true 326 | required: paramType === 'path' ? true : !!accepts.required, 327 | }; 328 | 329 | var schema = schemaBuilder.buildFromLoopBackType(accepts, typeRegistry, opts); 330 | if (paramType === 'body') { 331 | // HACK: Derive the type from model 332 | if (paramObject.name === 'data' && schema.type === 'object') { 333 | paramObject.schema = {$ref: typeRegistry.reference(classDef.name)}; 334 | } else { 335 | paramObject.schema = schema; 336 | } 337 | } else if (schema.type === 'file') { 338 | paramObject.type = 'file'; 339 | paramObject.in = 'formData'; 340 | paramObject.allowMultiple = false; 341 | paramObject.description = 'File to upload'; 342 | } else { 343 | var isComplexType = schema.type === 'object' || 344 | schema.type === 'array' || 345 | schema.$ref; 346 | if (isComplexType) { 347 | paramObject.type = 'string'; 348 | paramObject.format = 'JSON'; 349 | // TODO support array of primitive types 350 | // and map them to Swagger array of primitive types 351 | } else { 352 | _assign(paramObject, schema); 353 | } 354 | } 355 | 356 | return paramObject; 357 | }; 358 | }, 359 | }; 360 | 361 | function createUniqueOperationId(methodName, verb, path, operationIdRegistry) { 362 | // [bajtos] We used to remove leading model name from the operation 363 | // name for Swagger Spec 1.2. Swagger Spec 2.0 requires 364 | // operation ids to be unique, thus we have to include the model name. 365 | var id = methodName; 366 | 367 | if (!(id in operationIdRegistry)) { 368 | // The id is already unique 369 | return id; 370 | } 371 | 372 | var baseId = id; 373 | id = createLongOperationId(baseId, verb, path); 374 | if (id in operationIdRegistry) { 375 | g.warn('Warning: detected multiple remote methods ' + 376 | 'at the same HTTP endpoint. ' + 377 | '{{Swagger operation ids}} will NOT be unique.'); 378 | } 379 | 380 | // Rename the first operation so that all operation ids of 381 | // a multi-endpoint method are consistently using the long form 382 | if (operationIdRegistry[baseId]) { 383 | var oldEntry = operationIdRegistry[baseId]; 384 | var newId = createLongOperationId(baseId, oldEntry.method, oldEntry.path); 385 | oldEntry.operation.operationId = newId; 386 | operationIdRegistry[newId] = oldEntry; 387 | operationIdRegistry[baseId] = null; 388 | } 389 | 390 | return id; 391 | } 392 | 393 | function createLongOperationId(baseId, verb, path) { 394 | return baseId + '__' + verb + path.replace(/[\/:]+/g, '_'); 395 | } 396 | -------------------------------------------------------------------------------- /lib/specgen/schema-builder.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // Globalization 9 | var g = require('strong-globalize')(); 10 | 11 | var assert = require('assert'); 12 | var typeConverter = require('./type-converter'); 13 | 14 | var TYPES_PRIMITIVE = [ 15 | 'boolean', 16 | 'integer', 17 | 'number', 18 | 'null', 19 | 'string', 20 | 'object', 21 | 'array', 22 | 'file', 23 | ]; 24 | 25 | var KEY_TRANSLATIONS = { 26 | // LDL : Swagger 27 | min: 'minimum', 28 | max: 'maximum', 29 | length: 'maxLength', 30 | }; 31 | 32 | var SWAGGER_DATA_TYPE_FIELDS = [ 33 | 'format', 34 | 'default', 35 | 'enum', 36 | 'minimum', 37 | 'minItems', 38 | 'minLength', 39 | 'maximum', 40 | 'maxItems', 41 | 'maxLength', 42 | 'uniqueItems', 43 | 'pattern', 44 | ]; 45 | 46 | /** 47 | * Build a Swagger Schema Object and/or Parameter Object from LoopBack 48 | * type descriptor. 49 | * 50 | * @param {String|Function|Array|Object} ldlDef The loopback type to convert, 51 | * the value should be one of the following: 52 | * - a string value (type name), e.g. `'string'` or `'MyModel'` 53 | * - a constructor function, e.g. `String` or `MyModel` 54 | * - an array of a single item in `lbType` format 55 | * - an object containing a `type` property with string/function/array value 56 | * and validation fields like `length` or `max` 57 | * @param {TypeRegistry} typeRegistry The registry of known types and models. 58 | * @returns {Object} Swagger Schema Object that can be used as `schema` field 59 | * or as a base for Parameter Object. 60 | */ 61 | exports.buildFromLoopBackType = function(ldlDef, typeRegistry, opts) { 62 | assert(!!typeRegistry, 'typeRegistry is a required parameter'); 63 | 64 | // Normalize non-object values to object format `{ type: XYZ }` 65 | if (typeof ldlDef === 'string' || typeof ldlDef === 'function') { 66 | ldlDef = {type: ldlDef}; 67 | } else if (Array.isArray(ldlDef)) { 68 | ldlDef = {type: ldlDef}; 69 | } 70 | 71 | if (!ldlDef.type) { 72 | ldlDef = {type: 'any'}; 73 | } 74 | 75 | var schema = exports.buildMetadata(ldlDef); 76 | 77 | var ldlType = ldlDef.type; 78 | if (ldlType === 'object' && ldlDef.model) { 79 | // if createOnlyInstance is set, use an instance which is specifically 80 | // created for create operation. This instance will not contains excludeOnly 81 | // properties. For e.g generated "id" property. 82 | if (opts && opts.generateOperationScopedModels && ldlDef.createOnlyInstance) { 83 | ldlType = '$new_' + ldlDef.model; 84 | } else if (opts && opts.generateRelationProperties) { 85 | ldlType = ldlDef.model + 'WithRelations'; 86 | } else { 87 | ldlType = ldlDef.model; 88 | } 89 | } 90 | ldlType = exports.getLdlTypeName(ldlType); 91 | 92 | if (Array.isArray(ldlType)) { 93 | var itemLdl = ldlType[0] || 'any'; 94 | var itemSchema = exports.buildFromLoopBackType(itemLdl, typeRegistry); 95 | schema.type = 'array'; 96 | schema.items = itemSchema; 97 | return schema; 98 | } 99 | 100 | if (ldlType === 'object' && typeof ldlDef.type === 'object') { 101 | var obj = {}; 102 | for (var prop in ldlDef.type) { 103 | obj[prop] = exports.buildFromLoopBackType(ldlDef.type[prop], typeRegistry); 104 | } 105 | schema.type = 'object'; 106 | schema.properties = obj; 107 | return schema; 108 | } 109 | 110 | var ldlTypeLowerCase = ldlType.toLowerCase(); 111 | switch (ldlTypeLowerCase) { 112 | case 'date': 113 | schema.type = 'string'; 114 | schema.format = 'date-time'; 115 | break; 116 | case 'buffer': 117 | schema.type = 'string'; 118 | schema.format = 'byte'; 119 | break; 120 | case 'number': 121 | schema.type = 'number'; 122 | schema.format = schema.format || 'double'; // All JS numbers are doubles 123 | break; 124 | case 'any': 125 | schema.$ref = typeRegistry.reference('x-any'); 126 | break; 127 | default: 128 | if (exports.isPrimitiveType(ldlTypeLowerCase)) { 129 | schema.type = ldlTypeLowerCase; 130 | } else { 131 | // TODO - register anonymous types 132 | schema.$ref = typeRegistry.reference(ldlType); 133 | } 134 | } 135 | return schema; 136 | }; 137 | 138 | /** 139 | * @param {String|Function|Array|Object} ldlType LDL type 140 | * @returns {String|Array} Type name 141 | */ 142 | exports.getLdlTypeName = function(ldlType) { 143 | // Value "array" is a shortcut for `['any']` 144 | if (ldlType === 'array') { 145 | return ['any']; 146 | } 147 | 148 | if (typeof ldlType === 'string') { 149 | var arrayMatch = ldlType.match(/^\[(.*)\]$/); 150 | return arrayMatch ? [arrayMatch[1]] : ldlType; 151 | } 152 | 153 | if (typeof ldlType === 'function') { 154 | return ldlType.modelName || ldlType.name; 155 | } 156 | 157 | if (Array.isArray(ldlType)) { 158 | return ldlType; 159 | } 160 | 161 | if (typeof ldlType === 'object') { 162 | // Anonymous objects, they are allowed e.g. in accepts/returns definitions 163 | // TODO(bajtos) Build a named schema for this anonymous object 164 | return 'object'; 165 | } 166 | 167 | if (ldlType === undefined) { 168 | return 'any'; 169 | } 170 | 171 | var msg = g.f('Warning: unknown LDL type %j, using "{{any}}" instead', ldlType); 172 | console.error(msg); 173 | return 'any'; 174 | }; 175 | 176 | /** 177 | * Convert validations and other metadata from LDL format to Swagger format. 178 | * @param {Object} ldlDef LDL property/argument definition, 179 | * for example `{ type: 'string', maxLength: 64 }`. 180 | * @return {Object} Metadata in Swagger format. 181 | */ 182 | exports.buildMetadata = function(ldlDef) { 183 | var result = {}; 184 | var key; 185 | 186 | for (key in KEY_TRANSLATIONS) { 187 | if (key in ldlDef) { 188 | // Skip null as swagger 2.x UI does not support it 189 | // https://github.com/swagger-api/swagger-spec/issues/229 190 | if (ldlDef[key] != null) { 191 | result[KEY_TRANSLATIONS[key]] = ldlDef[key]; 192 | } 193 | } 194 | } 195 | 196 | /* eslint-disable one-var */ 197 | for (var ix in SWAGGER_DATA_TYPE_FIELDS) { 198 | key = SWAGGER_DATA_TYPE_FIELDS[ix]; 199 | if (key in ldlDef) 200 | result[key] = ldlDef[key]; 201 | } 202 | 203 | if ('default' in result) { 204 | // Skip null default values as the Swagger 2.x spec does not support null. 205 | // This is applied to both top-level and nested property defaults. 206 | // See: https://github.com/OAI/OpenAPI-Specification/issues/229 207 | if (result.default === null) { 208 | delete result.default; 209 | } else if (typeof result.default === 'object') { 210 | var deepNullFilter = function deepNullFilter(obj) { 211 | Object.keys(obj).forEach(function(key) { 212 | if (obj[key] === null) { 213 | delete obj[key]; 214 | } else if (typeof obj[key] === 'object') { 215 | deepNullFilter(obj[key]); 216 | } 217 | }); 218 | }; 219 | 220 | deepNullFilter(result.default); 221 | } 222 | } 223 | 224 | /* eslint-enable one-var */ 225 | if (ldlDef.description) { 226 | result.description = typeConverter.convertText(ldlDef.description); 227 | } else if (ldlDef.doc) { 228 | result.description = typeConverter.convertText(ldlDef.doc); 229 | } 230 | if (ldlDef.example) { 231 | result.example = ldlDef.example; 232 | } 233 | 234 | return result; 235 | }; 236 | 237 | exports.isPrimitiveType = function(typeName) { 238 | return TYPES_PRIMITIVE.indexOf(typeName.toLowerCase()) !== -1; 239 | }; 240 | -------------------------------------------------------------------------------- /lib/specgen/swagger-spec-generator.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // Globalization 9 | var g = require('strong-globalize')(); 10 | 11 | /** 12 | * Module dependencies. 13 | */ 14 | var path = require('path'); 15 | var _ = require('lodash'); 16 | var routeHelper = require('./route-helper'); 17 | var modelHelper = require('./model-helper'); 18 | var typeConverter = require('./type-converter'); 19 | var tagBuilder = require('./tag-builder'); 20 | var TypeRegistry = require('./type-registry'); 21 | 22 | /** 23 | * Create Swagger Object describing the API provided by loopbackApplication. 24 | * 25 | * @param {Application} loopbackApplication The application to document. 26 | * @param {Object} opts Options. 27 | * @returns {Object} 28 | */ 29 | module.exports = function createSwaggerObject(loopbackApplication, opts) { 30 | // We need a temporary REST adapter to discover our available routes. 31 | var remotes = loopbackApplication.remotes(); 32 | var adapter = remotes.handler('rest').adapter; 33 | var routes = adapter.allRoutes(); 34 | var classes = remotes.classes(); 35 | 36 | opts = opts || {}; 37 | var swaggerSpecExtensions = Object.assign({}, loopbackApplication.get('swagger')); 38 | 39 | var swaggerOptions = { 40 | generateOperationScopedModels: swaggerSpecExtensions.generateOperationScopedModels, 41 | generateRelationProperties: swaggerSpecExtensions.generateRelationProperties, 42 | }; 43 | 44 | // Prevent insert configurations into swagger definitions 45 | swaggerSpecExtensions = _.omit(swaggerSpecExtensions, [ 46 | 'generateOperationScopedModels', 47 | 'generateRelationProperties', 48 | ]); 49 | 50 | opts = _.defaults(opts, swaggerOptions, { 51 | basePath: loopbackApplication.get('restApiRoot') || '/api', 52 | // Default consumes/produces 53 | consumes: [ 54 | 'application/json', 55 | 'application/x-www-form-urlencoded', 56 | 'application/xml', 'text/xml', 57 | ], 58 | produces: [ 59 | 'application/json', 60 | 'application/xml', 'text/xml', 61 | // JSONP content types 62 | 'application/javascript', 'text/javascript', 63 | ], 64 | version: getPackagePropertyOrDefault('version', '1.0.0'), 65 | }); 66 | 67 | // Generate fixed fields like info and basePath 68 | var swaggerObject = generateSwaggerObjectBase(opts, swaggerSpecExtensions); 69 | 70 | var typeRegistry = new TypeRegistry(); 71 | var operationIdRegistry = Object.create(null); 72 | var loopbackRegistry = loopbackApplication.registry || 73 | loopbackApplication.loopback.registry || 74 | loopbackApplication.loopback; 75 | var models = loopbackRegistry.modelBuilder.models; 76 | for (var modelName in models) { 77 | modelHelper.registerModelDefinition(models[modelName], typeRegistry, opts); 78 | } 79 | 80 | // A class is an endpoint root; e.g. /users, /products, and so on. 81 | // In Swagger 2.0, there is no endpoint roots, but one can group endpoints 82 | // using tags. 83 | classes.forEach(function(aClass) { 84 | if (!aClass.name) return; 85 | 86 | var hasDocumentedMethods = aClass.methods().some(function(m) { 87 | return m.documented; 88 | }); 89 | if (!hasDocumentedMethods) return; 90 | 91 | swaggerObject.tags.push(tagBuilder.buildTagFromClass(aClass)); 92 | }); 93 | 94 | // A route is an endpoint, such as /users/findOne. 95 | routes.forEach(function(route) { 96 | if (!route.documented) return; 97 | 98 | // Get the class definition matching this route. 99 | var className = route.method.split('.')[0]; 100 | var classDef = classes.filter(function(item) { 101 | return item.name === className; 102 | })[0]; 103 | 104 | if (!classDef) { 105 | g.error('Route exists with no class: %j', route); 106 | return; 107 | } 108 | 109 | routeHelper.addRouteToSwaggerPaths(route, classDef, 110 | typeRegistry, operationIdRegistry, 111 | swaggerObject.paths, opts); 112 | }); 113 | 114 | _.assign(swaggerObject.definitions, typeRegistry.getDefinitions()); 115 | 116 | loopbackApplication.emit('swaggerResources', swaggerObject); 117 | return swaggerObject; 118 | }; 119 | 120 | /** 121 | * Generate a top-level resource doc. This is the entry point for swagger UI 122 | * and lists all of the available APIs. 123 | * @param {Object} opts Swagger options. 124 | * @return {Object} swaggerSpecExtensions swagger spec extensions. 125 | */ 126 | function generateSwaggerObjectBase(opts, swaggerSpecExtensions) { 127 | var apiInfo = _.cloneDeep(opts.apiInfo) || {}; 128 | for (var propertyName in apiInfo) { 129 | var property = apiInfo[propertyName]; 130 | apiInfo[propertyName] = typeConverter.convertText(property); 131 | } 132 | apiInfo.version = String(apiInfo.version || opts.version); 133 | if (!apiInfo.title) { 134 | apiInfo.title = getPackagePropertyOrDefault('name', 'LoopBack Application'); 135 | } 136 | 137 | if (!apiInfo.description) { 138 | apiInfo.description = getPackagePropertyOrDefault( 139 | 'description', 140 | 'LoopBack Application' 141 | ); 142 | } 143 | 144 | var basePath = opts.basePath; 145 | if (basePath && /\/$/.test(basePath)) 146 | basePath = basePath.slice(0, -1); 147 | 148 | return _.defaults({ 149 | swagger: '2.0', 150 | info: apiInfo, 151 | basePath: basePath, 152 | paths: {}, 153 | tags: [], 154 | }, swaggerSpecExtensions, { 155 | host: opts.host, 156 | schemes: opts.protocol ? [opts.protocol] : undefined, 157 | consumes: opts.consumes, 158 | produces: opts.produces, 159 | definitions: opts.models || {}, 160 | // TODO Authorizations (security, securityDefinitions) 161 | // TODO: responses, externalDocs 162 | }); 163 | } 164 | 165 | function getPackagePropertyOrDefault(name, defautValue) { 166 | try { 167 | var pkg = require(path.join(process.cwd(), 'package.json')); 168 | return pkg[name] || defautValue; 169 | } catch (e) { 170 | return defautValue; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /lib/specgen/tag-builder.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var typeConverter = require('./type-converter'); 9 | 10 | exports.buildTagFromClass = function(sharedClass) { 11 | var modelSettings = sharedClass.ctor && sharedClass.ctor.settings; 12 | var sharedCtor = sharedClass.ctor && sharedClass.ctor.sharedCtor; 13 | var swaggerSettings = modelSettings && modelSettings.swagger || {}; 14 | var name = swaggerSettings.tag && swaggerSettings.tag.name || sharedClass.name; 15 | 16 | var description = modelSettings && modelSettings.description || 17 | sharedCtor && sharedCtor.description; 18 | 19 | var externalDocs = swaggerSettings.tag && swaggerSettings.tag.externalDocs; 20 | 21 | return { 22 | name: name, 23 | description: typeConverter.convertText(description), 24 | externalDocs: externalDocs, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/specgen/type-converter.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var typeConverter = module.exports = { 9 | 10 | /** 11 | * Convert a text value that can be expressed either as a string or 12 | * as an array of strings. 13 | * @param {string|Array} value 14 | * @returns {string} 15 | */ 16 | convertText: function(value) { 17 | if (Array.isArray(value)) 18 | return value.join('\n'); 19 | return value; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/specgen/type-registry.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // Globalization 9 | var g = require('strong-globalize')(); 10 | 11 | var _ = require('lodash'); 12 | 13 | module.exports = TypeRegistry; 14 | 15 | function TypeRegistry() { 16 | this._definitions = Object.create(null); 17 | this._referenced = Object.create(null); 18 | this._loopbackTypeMap = Object.create(null); 19 | 20 | this.registerLoopbackType('x-any', {properties: {}}); 21 | this.registerLoopbackType('ObjectID', {type: 'string', pattern: '^[a-fA-F\\d]{24}$'}); 22 | this.registerLoopbackType('GeoPoint', { 23 | properties: { 24 | lat: {type: 'number'}, 25 | lng: {type: 'number'}, 26 | }, 27 | }); 28 | this.registerLoopbackType('DateString', {type: 'string', format: 'date-time'}); 29 | this.registerLoopbackType('file', {type: 'file'}); 30 | } 31 | 32 | TypeRegistry.prototype.registerLoopbackType = function(typeName, definition) { 33 | var typeNameLowerCase = typeName.toLowerCase(); 34 | this._loopbackTypeMap[typeNameLowerCase] = typeName; 35 | this._definitions[typeNameLowerCase] = definition; 36 | }; 37 | 38 | TypeRegistry.prototype.registerModel = function(typeName, definitionFn) { 39 | this._definitions[typeName] = definitionFn; 40 | }; 41 | 42 | TypeRegistry.prototype.reference = function(typeName) { 43 | var refName = typeName; 44 | var typeNameLowerCase = typeName.toLowerCase(); 45 | if (typeNameLowerCase in this._loopbackTypeMap) { 46 | refName = this._loopbackTypeMap[typeNameLowerCase]; 47 | } 48 | this._referenced[refName] = true; 49 | return '#/definitions/' + refName; 50 | }; 51 | 52 | TypeRegistry.prototype._buildDefinitionsFrom = function(definitionObj) { 53 | var defs = Object.create(null); 54 | var currentDefCount = 0; 55 | var newDefCount = 0; 56 | do { 57 | currentDefCount = Object.keys(definitionObj).length; 58 | for (var name in definitionObj) { 59 | var nameLowerCase = name.toLowerCase(); 60 | if (nameLowerCase in this._loopbackTypeMap) { 61 | var loopbackTypeName = this._loopbackTypeMap[nameLowerCase]; 62 | if (!defs[loopbackTypeName]) { 63 | defs[loopbackTypeName] = this._definitions[nameLowerCase]; 64 | } 65 | } else { 66 | if (!defs[name] && this._definitions[name]) { 67 | defs[name] = this._definitions[name](); 68 | } 69 | } 70 | } 71 | newDefCount = Object.keys(definitionObj).length; 72 | } while (currentDefCount !== newDefCount); 73 | return defs; 74 | }; 75 | 76 | TypeRegistry.prototype.getDefinitions = function() { 77 | var defs = this._buildDefinitionsFrom(this._referenced); 78 | 79 | for (var ref in this._referenced) { 80 | if (ref in defs) continue; 81 | // https://github.com/strongloop/loopback-explorer/issues/71 82 | g.warn('{{Swagger}}: skipping unknown type %j.', ref); 83 | } 84 | 85 | return defs; 86 | }; 87 | 88 | TypeRegistry.prototype.getAllDefinitions = function() { 89 | return this._buildDefinitionsFrom(this._definitions); 90 | }; 91 | 92 | TypeRegistry.prototype.isDefined = function(typeName) { 93 | var typeNameLowerCase = typeName.toLowerCase(); 94 | return typeName in this._definitions || 95 | typeNameLowerCase in this._loopbackTypeMap; 96 | }; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-swagger", 3 | "version": "5.9.0", 4 | "description": "Integration between LoopBack and Swagger API specs", 5 | "engines": { 6 | "node": ">=8" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "mocha", 11 | "lint": "eslint .", 12 | "posttest": "npm run lint" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/strongloop/loopback-swagger.git" 17 | }, 18 | "keywords": [ 19 | "StrongLoop Labs", 20 | "loopback", 21 | "api", 22 | "swagger" 23 | ], 24 | "author": "IBM Corp.", 25 | "readmeFilename": "README.md", 26 | "bugs": { 27 | "url": "https://github.com/strongloop/loopback-swagger/issues" 28 | }, 29 | "devDependencies": { 30 | "chai": "^4.1.2", 31 | "eslint": "^5.1.0", 32 | "eslint-config-loopback": "^11.0.0", 33 | "loopback": "^3.25.1", 34 | "mocha": "^5.2.0" 35 | }, 36 | "license": "MIT", 37 | "dependencies": { 38 | "async": "^2.1.4", 39 | "debug": "^3.1.0", 40 | "ejs": "^2.5.5", 41 | "lodash": "^4.17.11", 42 | "strong-globalize": "^4.1.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/codegen/json-schema.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | /* global describe, it */ 9 | var schemaParser = require('../../lib/codegen/json-schema'); 10 | var spec = require('./pet-expanded.json'); 11 | var expect = require('chai').expect; 12 | 13 | describe('json schema converter', function() { 14 | it('should handle allOf', function() { 15 | var models = schemaParser(spec.definitions); 16 | expect(models).have.property('pet'); 17 | expect(models).have.property('newPet'); 18 | expect(models).have.property('errorModel'); 19 | expect(models.pet.properties.tags.type).to.eql(['string']); 20 | expect(models.petGroup.relations.pets.type).to.eql('hasMany'); 21 | expect(models.petGroup.relations.pets.model).to.eql('pet'); 22 | expect(models.newPet).have.property('base', 'pet'); 23 | expect(models.newPet.properties).have.property('kind'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/codegen/pet-expanded.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://helloreverb.com/terms/", 8 | "contact": { 9 | "name": "Wordnik API Team", 10 | "email": "foo@example.com", 11 | "url": "http://madskristensen.net" 12 | }, 13 | "license": { 14 | "name": "MIT", 15 | "url": "http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT" 16 | } 17 | }, 18 | "host": "petstore.swagger.wordnik.com", 19 | "basePath": "/api/pet-app", 20 | "schemes": [ 21 | "http" 22 | ], 23 | "consumes": [ 24 | "application/json" 25 | ], 26 | "produces": [ 27 | "application/json" 28 | ], 29 | "x-implementation-templates": { 30 | "find": "${model}.find({limit: limit, where: {inq: tags}}, callback);" 31 | }, 32 | "paths": { 33 | "/pets": { 34 | "get": { 35 | "tags": ["Pet"], 36 | "x-implementation-template":{ 37 | "template":{ 38 | "$ref": "#/x-implementation-templates/find" 39 | }, 40 | "parameters": { 41 | "model": "Pet" 42 | } 43 | }, 44 | "description": "Returns all pets from the system that the user has access to", 45 | "operationId": "Pet.findPets", 46 | "produces": [ 47 | "application/json", 48 | "application/xml", 49 | "text/xml", 50 | "text/html" 51 | ], 52 | "parameters": [ 53 | { 54 | "name": "tags", 55 | "in": "query", 56 | "description": "tags to filter by", 57 | "required": false, 58 | "type": "array", 59 | "items": { 60 | "type": "string" 61 | }, 62 | "collectionFormat": "csv" 63 | }, 64 | { 65 | "name": "limit", 66 | "in": "query", 67 | "description": "maximum number of results to return", 68 | "required": false, 69 | "type": "integer", 70 | "format": "int32" 71 | } 72 | ], 73 | "responses": { 74 | "200": { 75 | "description": "pet response", 76 | "schema": { 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/definitions/pet" 80 | } 81 | } 82 | }, 83 | "default": { 84 | "description": "unexpected error", 85 | "schema": { 86 | "$ref": "#/definitions/errorModel" 87 | } 88 | } 89 | } 90 | }, 91 | "post": { 92 | "description": "Creates a new pet in the store. Duplicates are allowed", 93 | "operationId": "addPet", 94 | "x-operation-name": "createPet", 95 | "x-implementation" : [ 96 | "Pet.create(pet, callback);" 97 | ], 98 | "produces": [ 99 | "application/json" 100 | ], 101 | "parameters": [ 102 | { 103 | "name": "pet", 104 | "in": "body", 105 | "description": "Pet to add to the store", 106 | "required": true, 107 | "schema": { 108 | "$ref": "#/definitions/newPet" 109 | } 110 | } 111 | ], 112 | "responses": { 113 | "200": { 114 | "description": "pet response", 115 | "schema": { 116 | "$ref": "#/definitions/pet" 117 | } 118 | }, 119 | "default": { 120 | "description": "unexpected error", 121 | "schema": { 122 | "$ref": "#/definitions/errorModel" 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "/pets/{id}": { 129 | "get": { 130 | "description": "Returns a user based on a single ID, if the user does not have access to the pet", 131 | "operationId": "findPetById{id}", 132 | "x-implementation-template" : { 133 | "loopback": "Pet.findById(id, callback);" 134 | }, 135 | "produces": [ 136 | "application/json", 137 | "application/xml", 138 | "text/xml", 139 | "text/html" 140 | ], 141 | "parameters": [ 142 | { 143 | "name": "id", 144 | "in": "path", 145 | "description": "ID of pet to fetch", 146 | "required": true, 147 | "type": "integer", 148 | "format": "int64" 149 | } 150 | ], 151 | "responses": { 152 | "200": { 153 | "description": "pet response", 154 | "schema": { 155 | "$ref": "#/definitions/pet" 156 | } 157 | }, 158 | "default": { 159 | "description": "unexpected error", 160 | "schema": { 161 | "$ref": "#/definitions/errorModel" 162 | } 163 | } 164 | } 165 | }, 166 | "delete": { 167 | "description": "deletes a single pet based on the ID supplied", 168 | "operationId": "deletePet", 169 | "parameters": [ 170 | { 171 | "name": "id", 172 | "in": "path", 173 | "description": "ID of pet to delete", 174 | "required": true, 175 | "type": "integer", 176 | "format": "int64" 177 | } 178 | ], 179 | "responses": { 180 | "204": { 181 | "description": "pet deleted" 182 | }, 183 | "default": { 184 | "description": "unexpected error", 185 | "schema": { 186 | "$ref": "#/definitions/errorModel" 187 | } 188 | } 189 | } 190 | } 191 | } 192 | }, 193 | "definitions": { 194 | "stringList": { 195 | "type": "array", 196 | "items": { 197 | "type": "string" 198 | } 199 | }, 200 | "petGroup": { 201 | "properties": { 202 | "id": { 203 | "type": "integer", 204 | "format": "int64" 205 | } 206 | }, 207 | "x-relations": { 208 | "pets": { 209 | "partner": { 210 | "$ref": "#/definitions/pet" 211 | }, 212 | "type": "hasMany", 213 | "foreignKey": "groupId" 214 | } 215 | } 216 | }, 217 | "pet": { 218 | "required": [ 219 | "id", 220 | "name" 221 | ], 222 | "properties": { 223 | "id": { 224 | "type": "integer", 225 | "format": "int64" 226 | }, 227 | "name": { 228 | "type": "string" 229 | }, 230 | "tags": { 231 | "$ref": "#/definitions/stringList" 232 | } 233 | } 234 | }, 235 | "newPet": { 236 | "allOf": [ 237 | { 238 | "$ref": "#/definitions/pet" 239 | }, 240 | { 241 | "properties": { 242 | "id": { 243 | "type": "integer", 244 | "format": "int64" 245 | }, 246 | "kind": { 247 | "type": "string" 248 | } 249 | } 250 | } 251 | ] 252 | }, 253 | "errorModel": { 254 | "required": [ 255 | "code", 256 | "message" 257 | ], 258 | "properties": { 259 | "code": { 260 | "type": "integer", 261 | "format": "int32" 262 | }, 263 | "message": { 264 | "type": "string" 265 | } 266 | } 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /test/codegen/pet-with-embedded-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": 7 | "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 8 | "termsOfService": "http://helloreverb.com/terms/", 9 | "contact": { 10 | "name": "Wordnik API Team", 11 | "email": "foo@example.com", 12 | "url": "http://madskristensen.net" 13 | }, 14 | "license": { 15 | "name": "MIT", 16 | "url": "http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT" 17 | } 18 | }, 19 | "host": "petstore.swagger.wordnik.com", 20 | "basePath": "/api/pet-app", 21 | "schemes": ["http"], 22 | "consumes": ["application/json"], 23 | "produces": ["application/json"], 24 | "paths": { 25 | "/pets": { 26 | "get": { 27 | "tags": ["Pet"], 28 | "description": 29 | "Returns all pets from the system that the user has access to", 30 | "operationId": "Pet.findPets", 31 | "produces": [ 32 | "application/json", 33 | "application/xml", 34 | "text/xml", 35 | "text/html" 36 | ], 37 | "parameters": [ 38 | { 39 | "name": "x-tags", 40 | "in": "query", 41 | "description": "tags to filter by", 42 | "required": false, 43 | "type": "array", 44 | "items": { 45 | "type": "string" 46 | }, 47 | "collectionFormat": "csv" 48 | }, 49 | { 50 | "name": "x-limit", 51 | "in": "query", 52 | "description": "maximum number of results to return", 53 | "required": false, 54 | "type": "integer", 55 | "format": "int32" 56 | } 57 | ], 58 | "responses": { 59 | "200": { 60 | "description": "pet response", 61 | "schema": { 62 | "type": "array", 63 | "items": { 64 | "type": "object", 65 | "required": ["id", "name"], 66 | "properties": { 67 | "id": { 68 | "type": "integer", 69 | "format": "int64" 70 | }, 71 | "name": { 72 | "type": "string" 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/codegen/pet-with-refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://helloreverb.com/terms/", 8 | "contact": { 9 | "name": "Wordnik API Team", 10 | "email": "foo@example.com", 11 | "url": "http://madskristensen.net" 12 | }, 13 | "license": { 14 | "name": "MIT", 15 | "url": "http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT" 16 | } 17 | }, 18 | "host": "petstore.swagger.wordnik.com", 19 | "basePath": "/api/pet-app", 20 | "schemes": [ 21 | "http" 22 | ], 23 | "consumes": [ 24 | "application/json" 25 | ], 26 | "produces": [ 27 | "application/json" 28 | ], 29 | "x-implementation-templates": { 30 | "find": "${model}.find({limit: limit, where: {inq: tags}}, callback);" 31 | }, 32 | "paths": { 33 | "/pets": { 34 | "get": { 35 | "tags": ["Pet"], 36 | "x-implementation-template":{ 37 | "template":{ 38 | "$ref": "#/x-implementation-templates/find" 39 | }, 40 | "parameters": { 41 | "model": "Pet" 42 | } 43 | }, 44 | "description": "Returns all pets from the system that the user has access to", 45 | "operationId": "Pet.findPets", 46 | "produces": [ 47 | "application/json", 48 | "application/xml", 49 | "text/xml", 50 | "text/html" 51 | ], 52 | "parameters": [ 53 | { 54 | "name": "tags", 55 | "in": "query", 56 | "description": "tags to filter by", 57 | "required": false, 58 | "type": "array", 59 | "items": { 60 | "type": "string" 61 | }, 62 | "collectionFormat": "csv" 63 | }, 64 | { 65 | "name": "limit", 66 | "in": "query", 67 | "description": "maximum number of results to return", 68 | "required": false, 69 | "type": "integer", 70 | "format": "int32" 71 | } 72 | ], 73 | "responses": { 74 | "200": { 75 | "description": "pet response", 76 | "schema": { 77 | "type": "array", 78 | "items": { 79 | "$REF": "#/definitions/pet" 80 | } 81 | } 82 | }, 83 | "default": { 84 | "description": "unexpected error", 85 | "schema": { 86 | "$ref": "#/definitions/errorModel" 87 | } 88 | } 89 | } 90 | }, 91 | "post": { 92 | "description": "Creates a new pet in the store. Duplicates are allowed", 93 | "operationId": "addPet", 94 | "x-operation-name": "createPet", 95 | "x-implementation" : [ 96 | "Pet.create(pet, callback);" 97 | ], 98 | "produces": [ 99 | "application/json" 100 | ], 101 | "parameters": [ 102 | { 103 | "name": "pet", 104 | "in": "body", 105 | "description": "Pet to add to the store", 106 | "required": true, 107 | "schema": { 108 | "$ref": "#/definitions/newPet" 109 | } 110 | } 111 | ], 112 | "responses": { 113 | "200": { 114 | "description": "pet response", 115 | "schema": { 116 | "$ref": "#/definitions/pet" 117 | } 118 | }, 119 | "default": { 120 | "description": "unexpected error", 121 | "schema": { 122 | "$ref": "#/definitions/errorModel" 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "/pets/{id}": { 129 | "get": { 130 | "description": "Returns a user based on a single ID, if the user does not have access to the pet", 131 | "operationId": "findPetById{id}", 132 | "x-implementation-template" : { 133 | "loopback": "Pet.findById(id, callback);" 134 | }, 135 | "produces": [ 136 | "application/json", 137 | "application/xml", 138 | "text/xml", 139 | "text/html" 140 | ], 141 | "parameters": [ 142 | { 143 | "name": "id", 144 | "in": "path", 145 | "description": "ID of pet to fetch", 146 | "required": true, 147 | "type": "integer", 148 | "format": "int64" 149 | } 150 | ], 151 | "responses": { 152 | "200": { 153 | "description": "pet response", 154 | "schema": { 155 | "$ref": "#/definitions/pet" 156 | } 157 | }, 158 | "default": { 159 | "description": "unexpected error", 160 | "schema": { 161 | "$ref": "#/definitions/errorModel" 162 | } 163 | } 164 | } 165 | }, 166 | "delete": { 167 | "description": "deletes a single pet based on the ID supplied", 168 | "operationId": "deletePet", 169 | "parameters": [ 170 | { 171 | "name": "id", 172 | "in": "path", 173 | "description": "ID of pet to delete", 174 | "required": true, 175 | "type": "integer", 176 | "format": "int64" 177 | } 178 | ], 179 | "responses": { 180 | "204": { 181 | "description": "pet deleted" 182 | }, 183 | "default": { 184 | "description": "unexpected error", 185 | "schema": { 186 | "$ref": "#/definitions/errorModel" 187 | } 188 | } 189 | } 190 | } 191 | } 192 | }, 193 | "definitions": { 194 | "stringList": { 195 | "type": "array", 196 | "items": { 197 | "type": "string" 198 | } 199 | }, 200 | "petGroup": { 201 | "properties": { 202 | "id": { 203 | "type": "integer", 204 | "format": "int64" 205 | } 206 | }, 207 | "x-relations": { 208 | "pets": { 209 | "partner": { 210 | "$ref": "#/definitions/pet" 211 | }, 212 | "type": "hasMany", 213 | "foreignKey": "groupId" 214 | } 215 | } 216 | }, 217 | "pet": { 218 | "required": [ 219 | "id", 220 | "name" 221 | ], 222 | "properties": { 223 | "id": { 224 | "type": "integer", 225 | "format": "int64" 226 | }, 227 | "name": { 228 | "type": "string" 229 | }, 230 | "tags": { 231 | "$ref": "#/definitions/stringList" 232 | } 233 | } 234 | }, 235 | "newPet": { 236 | "allOf": [ 237 | { 238 | "$ref": "#/definitions/pet" 239 | }, 240 | { 241 | "properties": { 242 | "id": { 243 | "type": "integer", 244 | "format": "int64" 245 | }, 246 | "kind": { 247 | "type": "string" 248 | } 249 | } 250 | } 251 | ] 252 | }, 253 | "errorModel": { 254 | "required": [ 255 | "code", 256 | "message" 257 | ], 258 | "properties": { 259 | "code": { 260 | "type": "integer", 261 | "format": "int32" 262 | }, 263 | "message": { 264 | "type": "string" 265 | } 266 | } 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /test/codegen/pet-with-special-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://helloreverb.com/terms/", 8 | "contact": { 9 | "name": "Wordnik API Team", 10 | "email": "foo@example.com", 11 | "url": "http://madskristensen.net" 12 | }, 13 | "license": { 14 | "name": "MIT", 15 | "url": "http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT" 16 | } 17 | }, 18 | "host": "petstore.swagger.wordnik.com", 19 | "basePath": "/api/pet-app", 20 | "schemes": [ 21 | "http" 22 | ], 23 | "consumes": [ 24 | "application/json" 25 | ], 26 | "produces": [ 27 | "application/json" 28 | ], 29 | "x-implementation-templates": { 30 | "find": "${model}.find({limit: x_limit, where: {inq: tags}}, callback);" 31 | }, 32 | "paths": { 33 | "/pets": { 34 | "get": { 35 | "tags": ["Pet Controller"], 36 | "x-implementation-template":{ 37 | "template":{ 38 | "$ref": "#/x-implementation-templates/find" 39 | }, 40 | "parameters": { 41 | "model": "Pet" 42 | } 43 | }, 44 | "description": "Returns all pets from the system that the user has access to", 45 | "operationId": "Pet.findPets", 46 | "produces": [ 47 | "application/json", 48 | "application/xml", 49 | "text/xml", 50 | "text/html" 51 | ], 52 | "parameters": [ 53 | { 54 | "name": "tags", 55 | "in": "query", 56 | "description": "tags to filter by", 57 | "required": false, 58 | "type": "array", 59 | "items": { 60 | "type": "string" 61 | }, 62 | "collectionFormat": "csv" 63 | }, 64 | { 65 | "name": "x-limit", 66 | "in": "query", 67 | "description": "maximum number of results to return", 68 | "required": false, 69 | "type": "integer", 70 | "format": "int32" 71 | } 72 | ], 73 | "responses": { 74 | "200": { 75 | "description": "pet response", 76 | "schema": { 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/definitions/pet" 80 | } 81 | } 82 | }, 83 | "default": { 84 | "description": "unexpected error", 85 | "schema": { 86 | "$ref": "#/definitions/errorModel" 87 | } 88 | } 89 | } 90 | }, 91 | "post": { 92 | "description": "Creates a new pet in the store. Duplicates are allowed", 93 | "operationId": "addPet", 94 | "x-operation-name": "createPet", 95 | "x-implementation" : [ 96 | "Pet.create(pet, callback);" 97 | ], 98 | "produces": [ 99 | "application/json" 100 | ], 101 | "parameters": [ 102 | { 103 | "name": "pet", 104 | "in": "body", 105 | "description": "Pet to add to the store", 106 | "required": true, 107 | "schema": { 108 | "$ref": "#/definitions/newPet" 109 | } 110 | } 111 | ], 112 | "responses": { 113 | "200": { 114 | "description": "pet response", 115 | "schema": { 116 | "$ref": "#/definitions/pet" 117 | } 118 | }, 119 | "default": { 120 | "description": "unexpected error", 121 | "schema": { 122 | "$ref": "#/definitions/errorModel" 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "/pets/{id}": { 129 | "get": { 130 | "description": "Returns a user based on a single ID, if the user does not have access to the pet", 131 | "operationId": "findPetById{id}", 132 | "x-implementation-template" : { 133 | "loopback": "Pet.findById(id, callback);" 134 | }, 135 | "produces": [ 136 | "application/json", 137 | "application/xml", 138 | "text/xml", 139 | "text/html" 140 | ], 141 | "parameters": [ 142 | { 143 | "name": "id", 144 | "in": "path", 145 | "description": "ID of pet to fetch", 146 | "required": true, 147 | "type": "integer", 148 | "format": "int64" 149 | } 150 | ], 151 | "responses": { 152 | "200": { 153 | "description": "pet response", 154 | "schema": { 155 | "$ref": "#/definitions/pet" 156 | } 157 | }, 158 | "default": { 159 | "description": "unexpected error", 160 | "schema": { 161 | "$ref": "#/definitions/errorModel" 162 | } 163 | } 164 | } 165 | }, 166 | "delete": { 167 | "description": "deletes a single pet based on the ID supplied", 168 | "operationId": "deletePet", 169 | "parameters": [ 170 | { 171 | "name": "id", 172 | "in": "path", 173 | "description": "ID of pet to delete", 174 | "required": true, 175 | "type": "integer", 176 | "format": "int64" 177 | } 178 | ], 179 | "responses": { 180 | "204": { 181 | "description": "pet deleted" 182 | }, 183 | "default": { 184 | "description": "unexpected error", 185 | "schema": { 186 | "$ref": "#/definitions/errorModel" 187 | } 188 | } 189 | } 190 | } 191 | } 192 | }, 193 | "definitions": { 194 | "stringList": { 195 | "type": "array", 196 | "items": { 197 | "type": "string" 198 | } 199 | }, 200 | "petGroup": { 201 | "properties": { 202 | "id": { 203 | "type": "integer", 204 | "format": "int64" 205 | } 206 | }, 207 | "x-relations": { 208 | "pets": { 209 | "partner": { 210 | "$ref": "#/definitions/pet" 211 | }, 212 | "type": "hasMany", 213 | "foreignKey": "groupId" 214 | } 215 | } 216 | }, 217 | "pet": { 218 | "required": [ 219 | "id", 220 | "name" 221 | ], 222 | "properties": { 223 | "id": { 224 | "type": "integer", 225 | "format": "int64" 226 | }, 227 | "name": { 228 | "type": "string" 229 | }, 230 | "tags": { 231 | "$ref": "#/definitions/stringList" 232 | } 233 | } 234 | }, 235 | "newPet": { 236 | "allOf": [ 237 | { 238 | "$ref": "#/definitions/pet" 239 | }, 240 | { 241 | "properties": { 242 | "id": { 243 | "type": "integer", 244 | "format": "int64" 245 | }, 246 | "kind": { 247 | "type": "string" 248 | } 249 | } 250 | } 251 | ] 252 | }, 253 | "errorModel": { 254 | "required": [ 255 | "code", 256 | "message" 257 | ], 258 | "properties": { 259 | "code": { 260 | "type": "integer", 261 | "format": "int32" 262 | }, 263 | "message": { 264 | "type": "string" 265 | } 266 | } 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /test/codegen/pet-without-tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team" 10 | }, 11 | "license": { 12 | "name": "MIT" 13 | } 14 | }, 15 | "host": "petstore.swagger.io", 16 | "basePath": "/api", 17 | "schemes": [ 18 | "http" 19 | ], 20 | "consumes": [ 21 | "application/json" 22 | ], 23 | "produces": [ 24 | "application/json" 25 | ], 26 | "paths": { 27 | "/pets": { 28 | "get": { 29 | "description": "Returns all pets from the system that the user has access to", 30 | "operationId": "findPets", 31 | "produces": [ 32 | "application/json", 33 | "application/xml", 34 | "text/xml", 35 | "text/html" 36 | ], 37 | "parameters": [ 38 | { 39 | "name": "tags", 40 | "in": "query", 41 | "description": "tags to filter by", 42 | "required": false, 43 | "type": "array", 44 | "items": { 45 | "type": "string" 46 | }, 47 | "collectionFormat": "csv" 48 | }, 49 | { 50 | "name": "limit", 51 | "in": "query", 52 | "description": "maximum number of results to return", 53 | "required": false, 54 | "type": "integer", 55 | "format": "int32" 56 | } 57 | ], 58 | "responses": { 59 | "200": { 60 | "description": "pet response", 61 | "schema": { 62 | "type": "array", 63 | "items": { 64 | "$ref": "#/definitions/Pet" 65 | } 66 | } 67 | }, 68 | "default": { 69 | "description": "unexpected error", 70 | "schema": { 71 | "$ref": "#/definitions/ErrorModel" 72 | } 73 | } 74 | } 75 | }, 76 | "post": { 77 | "description": "Creates a new pet in the store. Duplicates are allowed", 78 | "operationId": "addPet", 79 | "produces": [ 80 | "application/json" 81 | ], 82 | "parameters": [ 83 | { 84 | "name": "pet", 85 | "in": "body", 86 | "description": "Pet to add to the store", 87 | "required": true, 88 | "schema": { 89 | "$ref": "#/definitions/NewPet" 90 | } 91 | } 92 | ], 93 | "responses": { 94 | "200": { 95 | "description": "pet response", 96 | "schema": { 97 | "$ref": "#/definitions/Pet" 98 | } 99 | }, 100 | "default": { 101 | "description": "unexpected error", 102 | "schema": { 103 | "$ref": "#/definitions/ErrorModel" 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "/pets/{id}": { 110 | "get": { 111 | "description": "Returns a user based on a single ID, if the user does not have access to the pet", 112 | "operationId": "findPetById", 113 | "produces": [ 114 | "application/json", 115 | "application/xml", 116 | "text/xml", 117 | "text/html" 118 | ], 119 | "parameters": [ 120 | { 121 | "name": "id", 122 | "in": "path", 123 | "description": "ID of pet to fetch", 124 | "required": true, 125 | "type": "integer", 126 | "format": "int64" 127 | } 128 | ], 129 | "responses": { 130 | "200": { 131 | "description": "pet response", 132 | "schema": { 133 | "$ref": "#/definitions/Pet" 134 | } 135 | }, 136 | "default": { 137 | "description": "unexpected error", 138 | "schema": { 139 | "$ref": "#/definitions/ErrorModel" 140 | } 141 | } 142 | } 143 | }, 144 | "delete": { 145 | "description": "deletes a single pet based on the ID supplied", 146 | "operationId": "deletePet", 147 | "parameters": [ 148 | { 149 | "name": "id", 150 | "in": "path", 151 | "description": "ID of pet to delete", 152 | "required": true, 153 | "type": "integer", 154 | "format": "int64" 155 | } 156 | ], 157 | "responses": { 158 | "204": { 159 | "description": "pet deleted" 160 | }, 161 | "default": { 162 | "description": "unexpected error", 163 | "schema": { 164 | "$ref": "#/definitions/ErrorModel" 165 | } 166 | } 167 | } 168 | } 169 | } 170 | }, 171 | "definitions": { 172 | "Pet": { 173 | "type": "object", 174 | "allOf": [ 175 | { 176 | "$ref": "#/definitions/NewPet" 177 | }, 178 | { 179 | "required": [ 180 | "id" 181 | ], 182 | "properties": { 183 | "id": { 184 | "type": "integer", 185 | "format": "int64" 186 | } 187 | } 188 | } 189 | ] 190 | }, 191 | "NewPet": { 192 | "type": "object", 193 | "required": [ 194 | "name" 195 | ], 196 | "properties": { 197 | "name": { 198 | "type": "string" 199 | }, 200 | "tag": { 201 | "type": "string" 202 | } 203 | } 204 | }, 205 | "ErrorModel": { 206 | "type": "object", 207 | "required": [ 208 | "code", 209 | "message" 210 | ], 211 | "properties": { 212 | "code": { 213 | "type": "integer", 214 | "format": "int32" 215 | }, 216 | "message": { 217 | "type": "string" 218 | } 219 | } 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /test/codegen/swagger-v12.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | var V12Generator = require('../../lib/codegen/generator-v1.2.js'); 10 | 11 | function generate(spec, options) { 12 | var generator = new V12Generator(); 13 | return generator.generateRemoteMethods(spec, options); 14 | } 15 | 16 | describe('Swagger spec v1.2 generator', function() { 17 | it('generates remote methods', function() { 18 | var petStoreV12Spec = require('../../example/pet-store-1.2.json'); 19 | var code = generate(petStoreV12Spec, {modelName: 'Store'}); 20 | expect(code.Store).to.be.a('string'); 21 | }); 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /test/codegen/swagger-v2.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | var V2Generator = require('../../lib/codegen/generator-v2'); 10 | var generateModels = require('../../index').generateModels; 11 | 12 | var petStoreV2Spec = require('../../example/pet-store-2.0.json'); 13 | var pet2 = require('./pet-expanded.json'); 14 | var pet3 = require('./pet-without-tags.json'); 15 | var pet4 = require('./pet-with-embedded-schema.json'); 16 | var note = require('./note.json'); 17 | var pet5 = require('./pet-with-refs.json'); 18 | var pet6 = require('./pet-with-special-names.json'); 19 | var generator = new V2Generator(); 20 | 21 | describe('Swagger spec v2 generator', function() { 22 | it('generates remote methods', function() { 23 | var code = generator.generateRemoteMethods(petStoreV2Spec, 24 | {modelName: 'Store'}); 25 | expect(code.store).to.be.a('string'); 26 | }); 27 | 28 | it('generates remote methods without tags', function() { 29 | generator.mapTagsToModels(pet3); 30 | var models = generateModels(pet3); 31 | expect(models.SwaggerModel).to.be.a('object'); 32 | var code = generator.generateRemoteMethods(pet3); 33 | expect(code.SwaggerModel).to.be.a('string'); 34 | expect(Object.keys(code)).to.eql(['SwaggerModel']); 35 | }); 36 | 37 | it('generates remote methods with special names', function() { 38 | generator.mapTagsToModels(pet6); 39 | var models = generateModels(pet6); 40 | expect(models.Pet_Controller).to.be.a('object'); 41 | var code = generator.generateRemoteMethods(pet6); 42 | expect(code.Pet_Controller).to.be.a('string'); 43 | var petController = code.Pet_Controller; 44 | expect(petController).to.contain( 45 | 'Pet_Controller.petFindPets = function(tags, x_limit, callback) {' 46 | ); 47 | expect(petController).to.contain( 48 | 'Pet.find({limit: x_limit, where: {inq: tags}}, callback);' 49 | ); 50 | }); 51 | 52 | it('parse operations', function() { 53 | var operations = generator.getOperations(pet2); 54 | expect(operations['/pet-app/pets'].get.returns).to.eql( 55 | [{ 56 | description: 'pet response', 57 | type: ['pet'], 58 | arg: 'data', 59 | root: true, 60 | }] 61 | ); 62 | }); 63 | 64 | it('parse operations with $REF', function() { 65 | var operations = generator.getOperations(pet5); 66 | expect(operations['/pet-app/pets'].get.returns).to.eql( 67 | [{ 68 | description: 'pet response', 69 | type: ['pet'], 70 | arg: 'data', 71 | root: true, 72 | }] 73 | ); 74 | }); 75 | 76 | it('generates remote methods from expanded spec', function() { 77 | var code = generator.generateRemoteMethods(pet2, 78 | {modelName: 'Pet'}).Pet; 79 | expect(code).contain('Pet.findPets = function(tags, limit, callback)'); 80 | expect(code).contain('Pet.remoteMethod(\'findPets\''); 81 | expect(code).contain('Pet.findPetByIdId = function(id, callback)'); 82 | expect(code).contain('Pet.remoteMethod(\'findPetByIdId\''); 83 | expect(code).contain('Pet.deletePet = function(id, callback)'); 84 | expect(code).contain('Pet.remoteMethod(\'deletePet\''); 85 | expect(code).contain('Pet.createPet = function(pet, callback)'); 86 | expect(code).contain('Pet.remoteMethod(\'createPet\''); 87 | expect(code).contain('type: [ \'pet\' ],'); 88 | expect(code).contain( 89 | 'Pet.find({limit: limit, where: {inq: tags}}, callback);' 90 | ); 91 | expect(code).contain('Pet.create(pet, callback);'); 92 | expect(code).contain('Pet.findById(id, callback);'); 93 | }); 94 | 95 | it('generates remote methods with tags', function() { 96 | var code = generator.generateRemoteMethods(note, {}); 97 | expect(Object.keys(code)).eql(['User', 'Note']); 98 | }); 99 | 100 | it('transform operations', function() { 101 | var operations = generator.getOperations(petStoreV2Spec); 102 | expect(operations).to.have.property('/createWithList'); 103 | expect(operations['/createWithList']).to.have.property('post'); 104 | var op = operations['/createWithList']['post']; 105 | expect(op.operationId).to.eql('createUsersWithListInput'); 106 | }); 107 | 108 | it('generates remote methods without definitions', function() { 109 | var code = generator.generateRemoteMethods(pet4, 110 | {modelName: 'Pet'}); 111 | expect(code.Pet).contain( 112 | 'Pet.findPets = function(x_tags, x_limit, callback)' 113 | ); 114 | }); 115 | 116 | it('generates embedded models', function() { 117 | var code = generator.generateRemoteMethods(pet4); 118 | expect(pet4.definitions).to.eql({ 119 | 'findPets_response_200': { 120 | name: 'findPets_response_200', 121 | properties: { 122 | id: { 123 | type: 'number', required: true, format: 'int64', 124 | }, 125 | name: { 126 | type: 'string', required: true, 127 | }, 128 | }, 129 | }, 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | -------------------------------------------------------------------------------- /test/specgen/fixtures/dummy-swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | custom index.html 2 | -------------------------------------------------------------------------------- /test/specgen/fixtures/dummy-swagger-ui/swagger-ui.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /* custom swagger-ui file */ 7 | -------------------------------------------------------------------------------- /test/specgen/model-helper.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var modelHelper = require('../../lib/specgen/model-helper'); 9 | var TypeRegistry = require('../../lib/specgen/type-registry'); 10 | var _defaults = require('lodash').defaults; 11 | var loopback = require('loopback'); 12 | var expect = require('chai').expect; 13 | 14 | describe('model-helper', function() { 15 | describe('related models', function() { 16 | it('should include related models', function() { 17 | var defs = buildSwaggerModelsWithRelations({ 18 | str: String, // 'string' 19 | }); 20 | expect(defs).has.property('testModel'); 21 | expect(defs).has.property('relatedModel'); 22 | }); 23 | 24 | it('should include nesting models', function() { 25 | var Model2 = loopback.createModel('Model2', {street: String}); 26 | var Model1 = loopback.createModel('Model1', { 27 | str: String, // 'string' 28 | address: Model2, 29 | }, {models: {Model2: Model2}}); 30 | var defs = getDefinitionsForModel(Model1); 31 | expect(defs).has.property('Model1'); 32 | expect(defs).has.property('Model2'); 33 | }); 34 | 35 | it('should include used models', function() { 36 | var Model4 = loopback.createModel('Model4', {street: String}); 37 | var Model3 = loopback.createModel('Model3', { 38 | str: String, // 'string' 39 | }, {models: {model4: 'Model4'}}); 40 | var defs = getDefinitionsForModel(Model3); 41 | expect(defs).has.property('Model3'); 42 | expect(defs).has.property('Model4'); 43 | }); 44 | 45 | it('should include nesting models in array', function() { 46 | var Model6 = loopback.createModel('Model6', {street: String}); 47 | var Model5 = loopback.createModel('Model5', { 48 | str: String, // 'string' 49 | addresses: [Model6], 50 | }, {models: {Model6: Model6}}); 51 | var defs = getDefinitionsForModel(Model5); 52 | expect(defs).has.property('Model5'); 53 | expect(defs).has.property('Model6'); 54 | }); 55 | 56 | // https://github.com/strongloop/loopback-explorer/issues/49 57 | it('should work if Array class is extended and no related models are found', 58 | function() { 59 | var Model7 = loopback.createModel('Model7', {street: String}); 60 | Array.prototype.customFunc = function() { 61 | }; 62 | var defs = getDefinitionsForModel(Model7); 63 | expect(defs).has.property('Model7'); 64 | expect(Object.keys(defs)).has.property('length', 1); 65 | }); 66 | 67 | // https://github.com/strongloop/loopback-explorer/issues/71 68 | it('should skip unknown types', function() { 69 | var Model8 = loopback.createModel('Model8', { 70 | patient: { 71 | model: 'physician', 72 | type: 'hasMany', 73 | through: 'appointment', 74 | }, 75 | }); 76 | var defs = getDefinitionsForModel(Model8); 77 | // Hack: prevent warnings in other tests caused by global model registry 78 | Model8.definition.rawProperties.patient.type = 'string'; 79 | Model8.definition.properties.patient.type = 'string'; 80 | 81 | expect(Object.keys(defs)).to.not.contain('hasMany'); 82 | }); 83 | }); 84 | 85 | describe('examples', function() { 86 | it('supports setting an example model', function() { 87 | var aClass = createModelCtor({ 88 | content: 'string', 89 | }); 90 | aClass.ctor.definition.settings = { 91 | swagger: { 92 | example: {content: 'Hello world!'}, 93 | }, 94 | }; 95 | var def = getDefinitionsForModel(aClass.ctor).testModel; 96 | expect(def).to.have.property('example').and.to.eql( 97 | {content: 'Hello world!'} 98 | ); 99 | }); 100 | }); 101 | 102 | describe('hidden properties', function() { 103 | it('should hide properties marked as "hidden"', function() { 104 | var aClass = createModelCtor({ 105 | visibleProperty: 'string', 106 | hiddenProperty: 'string', 107 | }); 108 | aClass.ctor.definition.settings = { 109 | hidden: ['hiddenProperty'], 110 | }; 111 | var def = getDefinitionsForModel(aClass.ctor).testModel; 112 | expect(def.properties).to.not.have.property('hiddenProperty'); 113 | expect(def.properties).to.have.property('visibleProperty'); 114 | }); 115 | }); 116 | 117 | it('should convert top level array description to string', function() { 118 | var model = {}; 119 | model.definition = { 120 | name: 'test', 121 | description: ['1', '2', '3'], 122 | properties: {}, 123 | }; 124 | var defs = getDefinitionsForModel(model); 125 | expect(defs.test.description).to.equal('1\n2\n3'); 126 | }); 127 | 128 | it('should convert property level array description to string', function() { 129 | var model = {}; 130 | model.definition = { 131 | name: 'test', 132 | properties: { 133 | prop1: { 134 | type: 'string', 135 | description: ['1', '2', '3'], 136 | }, 137 | }, 138 | }; 139 | var defs = getDefinitionsForModel(model); 140 | expect(defs.test.properties.prop1.description).to.equal('1\n2\n3'); 141 | }); 142 | 143 | it('omits empty "required" array', function() { 144 | var aClass = createModelCtor({}); 145 | var def = getDefinitionsForModel(aClass.ctor).testModel; 146 | expect(def).to.not.have.property('required'); 147 | }); 148 | 149 | describe('property converter', function() { 150 | it('converts properties with no type to type "any"', function() { 151 | var model = {}; 152 | model.definition = { 153 | name: 'test', 154 | properties: { 155 | questionIndex: { 156 | required: true, 157 | }, 158 | }, 159 | }; 160 | var defs = getDefinitionsForModel(model); 161 | expect(defs.test.properties.questionIndex).to.have.property('$ref', '#/definitions/x-any'); 162 | }); 163 | 164 | it('converts array properties with no type to an array of type "any"', function() { 165 | var model = {}; 166 | model.definition = { 167 | name: 'test', 168 | properties: { 169 | questions: [{ 170 | required: true, 171 | }], 172 | }, 173 | }; 174 | var defs = getDefinitionsForModel(model); 175 | expect(defs.test.properties.questions).to.have.property('type', 'array'); 176 | expect(defs.test.properties.questions).to.have.property('items').eql({'$ref': '#/definitions/x-any'}); 177 | }); 178 | 179 | it('generates custom extentions for swagger', function() { 180 | var model = {}; 181 | model.definition = { 182 | name: 'test', 183 | settings: { 184 | 'x-myDef': 'myCustomDef', 185 | 'xIgnore': 'ignoreThisDef', 186 | }, 187 | properties: {}, 188 | }; 189 | var defs = getDefinitionsForModel(model); 190 | expect(defs.test) 191 | .to.have.property('x-myDef'); 192 | expect(defs.test) 193 | .to.not.have.property('xIgnore'); 194 | }); 195 | }); 196 | }); 197 | 198 | // Simulates the format of a remoting class. 199 | function buildSwaggerModels(modelProperties, modelOptions) { 200 | var aClass = createModelCtor(modelProperties, modelOptions); 201 | return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel; 202 | } 203 | 204 | function createModelCtor(properties, modelOptions) { 205 | Object.keys(properties).forEach(function(name) { 206 | var type = properties[name]; 207 | if (typeof type !== 'object' || Array.isArray(type)) 208 | properties[name] = {type: type}; 209 | }); 210 | 211 | var definition = { 212 | name: 'testModel', 213 | properties: properties, 214 | }; 215 | _defaults(definition, modelOptions); 216 | 217 | var aClass = { 218 | ctor: { 219 | definition: definition, 220 | }, 221 | }; 222 | return aClass; 223 | } 224 | 225 | function buildSwaggerModelsWithRelations(model) { 226 | Object.keys(model).forEach(function(name) { 227 | model[name] = {type: model[name]}; 228 | }); 229 | // Mock up the related model 230 | var relatedModel = { 231 | definition: { 232 | name: 'relatedModel', 233 | properties: { 234 | fk: String, 235 | }, 236 | }, 237 | }; 238 | var aClass = { 239 | ctor: { 240 | definition: { 241 | name: 'testModel', 242 | properties: model, 243 | }, 244 | // Mock up relations 245 | relations: { 246 | other: { 247 | modelTo: relatedModel, 248 | }, 249 | }, 250 | }, 251 | }; 252 | 253 | var registry = new TypeRegistry(); 254 | modelHelper.registerModelDefinition(aClass.ctor, registry); 255 | return registry.getAllDefinitions(); 256 | } 257 | 258 | function getDefinitionsForModel(modelCtor) { 259 | var registry = new TypeRegistry(); 260 | modelHelper.registerModelDefinition(modelCtor, registry); 261 | registry.reference(modelCtor.modelName || modelCtor.definition.name); 262 | return registry.getDefinitions(); 263 | } 264 | -------------------------------------------------------------------------------- /test/specgen/schema-builder.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var schemaBuilder = require('../../lib/specgen/schema-builder'); 9 | var TypeRegistry = require('../../lib/specgen/type-registry'); 10 | var format = require('util').format; 11 | var _defaults = require('lodash').defaults; 12 | var loopback = require('loopback'); 13 | var expect = require('chai').expect; 14 | 15 | var ANY_TYPE = {$ref: '#/definitions/x-any'}; 16 | 17 | describe('schema-builder', function() { 18 | describeTestCases('for constructor types', [ 19 | {in: String, out: {type: 'string'}}, 20 | {in: Number, out: {type: 'number', format: 'double'}}, 21 | {in: Date, out: {type: 'string', format: 'date-time'}}, 22 | {in: Boolean, out: {type: 'boolean'}}, 23 | {in: Buffer, out: {type: 'string', format: 'byte'}}, 24 | ]); 25 | 26 | describeTestCases('for string types', [ 27 | {in: 'string', out: {type: 'string'}}, 28 | {in: 'number', out: {type: 'number', format: 'double'}}, 29 | {in: 'date', out: {type: 'string', format: 'date-time'}}, 30 | {in: 'boolean', out: {type: 'boolean'}}, 31 | {in: 'buffer', out: {type: 'string', format: 'byte'}}, 32 | ]); 33 | 34 | describeTestCases('for array definitions', [ 35 | {in: [String], 36 | out: {type: 'array', items: {type: 'string'}}}, 37 | {in: ['string'], 38 | out: {type: 'array', items: {type: 'string'}}}, 39 | {in: [{type: 'string', maxLength: 64}], 40 | out: {type: 'array', items: {type: 'string', maxLength: 64}}}, 41 | {in: [{type: 'date'}], 42 | out: {type: 'array', items: {type: 'string', format: 'date-time'}}}, 43 | {in: [], 44 | out: {type: 'array', items: ANY_TYPE}}, 45 | // This value is somehow provided by loopback-boot called from 46 | // loopback-workspace. 47 | {in: [undefined], 48 | out: {type: 'array', items: ANY_TYPE}}, 49 | {in: 'array', 50 | out: {type: 'array', items: ANY_TYPE}}, 51 | ]); 52 | 53 | describeTestCases('for complex types', [ 54 | // Note: User is a built-in loopback model 55 | {in: loopback.User, 56 | out: {$ref: '#/definitions/User'}}, 57 | {in: {type: 'User'}, 58 | out: {$ref: '#/definitions/User'}}, 59 | {in: {type: 'user'}, 60 | out: {$ref: '#/definitions/user'}}, 61 | // Anonymous type 62 | {in: { 63 | type: { 64 | foo: 'string', 65 | bar: 'number', 66 | quux: { 67 | type: { 68 | quuux: 'date', 69 | }, 70 | }, 71 | }, 72 | }, 73 | out: { 74 | type: 'object', 75 | properties: { 76 | foo: { 77 | type: 'string', 78 | }, 79 | bar: { 80 | type: 'number', 81 | format: 'double', 82 | }, 83 | quux: { 84 | properties: { 85 | quuux: { 86 | format: 'date-time', 87 | type: 'string', 88 | }, 89 | }, 90 | type: 'object', 91 | }, 92 | }, 93 | }}, 94 | {in: { 95 | type: { 96 | foo: 'string', 97 | bar: 'number', 98 | }, 99 | example: { 100 | foo: 'something', 101 | bar: 'something else', 102 | }, 103 | }, 104 | out: { 105 | type: 'object', 106 | properties: { 107 | foo: { 108 | type: 'string', 109 | }, 110 | bar: { 111 | type: 'number', 112 | format: 'double', 113 | }, 114 | }, 115 | example: { 116 | foo: 'something', 117 | bar: 'something else', 118 | }, 119 | }}, 120 | {in: { 121 | type: { 122 | foo: { 123 | type: 'string', 124 | example: 'hello world', 125 | }, 126 | }, 127 | }, 128 | out: { 129 | type: 'object', 130 | properties: { 131 | foo: { 132 | type: 'string', 133 | example: 'hello world', 134 | }, 135 | }, 136 | }}, 137 | ]); 138 | 139 | describeTestCases('for extra metadata', [ 140 | {in: {type: String, doc: 'a-description'}, 141 | out: {type: 'string', description: 'a-description'}}, 142 | {in: {type: String, doc: ['line1', 'line2']}, 143 | out: {type: 'string', description: 'line1\nline2'}}, 144 | {in: {type: String, description: 'a-description'}, 145 | out: {type: 'string', description: 'a-description'}}, 146 | {in: {type: String, description: ['line1', 'line2']}, 147 | out: {type: 'string', description: 'line1\nline2'}}, 148 | {in: {type: String, required: true}, 149 | out: {type: 'string'}}, // the flag required is handled specially 150 | {in: {type: String, length: 10}, 151 | out: {type: 'string', maxLength: 10}}, 152 | {in: {type: String, length: null}, 153 | out: {type: 'string'}}, 154 | {in: {type: String, default: 'default-value'}, 155 | out: {type: 'string', default: 'default-value'}}, 156 | {in: {type: String, default: null}, 157 | out: {type: 'string'}}, 158 | {in: {type: String, default: {aaa: 'default-key-val'}}, 159 | out: {type: 'string', default: {aaa: 'default-key-val'}}}, 160 | {in: {type: String, 161 | default: { 162 | aaa: 'default-key-val', 163 | bbb: null, 164 | ccc: {ddd: 'val', eee: null}}}, 165 | out: {type: 'string', default: {aaa: 'default-key-val', ccc: {ddd: 'val'}}}}, 166 | ]); 167 | 168 | describeTestCases('for built-in LoopBack types', [ 169 | {in: {type: 'ObjectID', doc: 'a-description'}, 170 | out: {$ref: '#/definitions/ObjectID', description: 'a-description'}}, 171 | {in: {type: 'objectid', doc: 'a-description'}, 172 | out: {$ref: '#/definitions/ObjectID', description: 'a-description'}}, 173 | {in: {type: 'OBJECTID', doc: 'a-description'}, 174 | out: {$ref: '#/definitions/ObjectID', description: 'a-description'}}, 175 | {in: {type: 'GeoPoint', doc: '{lng: 10, lat: 20}'}, 176 | out: {$ref: '#/definitions/GeoPoint', description: '{lng: 10, lat: 20}'}}, 177 | {in: {type: 'geopoint', doc: '{lng: 10, lat: 20}'}, 178 | out: {$ref: '#/definitions/GeoPoint', description: '{lng: 10, lat: 20}'}}, 179 | {in: {type: 'GEOPOINT', doc: '{lng: 10, lat: 20}'}, 180 | out: {$ref: '#/definitions/GeoPoint', description: '{lng: 10, lat: 20}'}}, 181 | {in: {type: 'DateString'}, 182 | out: {$ref: '#/definitions/DateString'}}, 183 | {in: {type: 'datestring'}, 184 | out: {$ref: '#/definitions/DateString'}}, 185 | {in: {type: 'DATESTRING'}, 186 | out: {$ref: '#/definitions/DateString'}}, 187 | ]); 188 | 189 | function describeTestCases(name, testCases) { 190 | describe(name, function() { 191 | testCases.forEach(function(tc) { 192 | var inStr = formatType(tc.in); 193 | var outStr = formatType(tc.out); 194 | it(format('converts %s to %s', inStr, outStr), function() { 195 | var registry = new TypeRegistry(); 196 | var schema = schemaBuilder.buildFromLoopBackType(tc.in, registry); 197 | expect(schema).to.eql(tc.out); 198 | }); 199 | }); 200 | }); 201 | } 202 | 203 | function formatType(type) { 204 | if (Array.isArray(type)) 205 | return '[' + type.map(formatType) + ']'; 206 | 207 | if (typeof type === 'function') 208 | return type.modelName ? 209 | 'model ' + type.modelName : 210 | 'ctor ' + type.name; 211 | 212 | return format(type); 213 | } 214 | }); 215 | -------------------------------------------------------------------------------- /test/specgen/tag-builder.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2019. All Rights Reserved. 2 | // Node module: loopback-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var tagBuilder = require('../../lib/specgen/tag-builder'); 9 | var expect = require('chai').expect; 10 | var _defaults = require('lodash').defaults; 11 | 12 | describe('tag-builder', function() { 13 | it('joins array descriptions from ctor.settings', function() { 14 | var tag = tagBuilder.buildTagFromClass({ 15 | ctor: {settings: {description: ['line1', 'line2']}}, 16 | }); 17 | 18 | expect(tag.description).to.equal('line1\nline2'); 19 | }); 20 | 21 | it('joins array descriptions from ctor.sharedCtor', function() { 22 | var tag = tagBuilder.buildTagFromClass({ 23 | ctor: {sharedCtor: {description: ['1', '2', '3']}}, 24 | }); 25 | 26 | expect(tag.description).to.eql('1\n2\n3'); 27 | }); 28 | 29 | it('should use custom swagger name if provided', function() { 30 | var tag = tagBuilder.buildTagFromClass({ 31 | ctor: {settings: {swagger: {tag: {name: 'Something Else'}}}}, 32 | }); 33 | 34 | expect(tag.name).to.eql('Something Else'); 35 | }); 36 | 37 | it('sets external spec properties from model options', function() { 38 | var tag = tagBuilder.buildTagFromClass({ 39 | ctor: {settings: {swagger: {tag: {externalDocs: {url: 'http://google.com'}}}}}, 40 | }); 41 | 42 | expect(tag.externalDocs).to.eql({url: 'http://google.com'}); 43 | }); 44 | }); 45 | --------------------------------------------------------------------------------