├── .commitlintrc.js ├── .editorconfig ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .huskyrc.json ├── .jshintrc ├── .npmignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── bin ├── browserify.sh ├── start-test-server.js └── watchify.sh ├── bower.json ├── browser ├── example │ ├── README.md │ ├── assets │ │ ├── angular-sanitize.js │ │ ├── angular.js │ │ ├── bootstrap.min.css │ │ ├── jquery-2.0.2.min.js │ │ ├── spinner.gif │ │ └── traverson-hal.min.js │ ├── browserify │ │ ├── angular │ │ │ ├── angular-common-js.js │ │ │ └── angular.js │ │ ├── browserify.sh │ │ ├── bundle.js │ │ ├── example.js │ │ ├── index.html │ │ ├── package.json │ │ └── watchify.sh │ ├── github-example.js │ ├── github.html │ ├── hal.html │ ├── hal.js │ ├── index.html │ └── traverson-angular-example.js └── test │ ├── README.md │ └── index.html ├── greenkeeper.json ├── howto_release.markdown ├── package-lock.json ├── package.json ├── readme.markdown ├── test ├── abort_request.js ├── angular_test_helper.js ├── browser_suite.js ├── continue.js ├── json_get_resource.js ├── json_requests.js ├── localhost.js ├── using_angular_mocks.js └── util.js └── traverson-angular.js /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['traverson']}; 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | browser/example/browserify/bundle.js -diff 2 | 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. I am using the following Traverson call (please provide the full code snippet and also how you initialize `traverson`/`traverson-angular`) '...' 13 | 2. This results in the following (describe the unexpected behaviour you are seeing and/or the error message you get) '....' 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Node.js (please complete the following information):** 19 | - OS: [e.g. Windows 10, MacOS 10.14, Linux distro] 20 | - Node.js version: 21 | - Traverson Version: 22 | - traverson-angular Version: 23 | 24 | **Browser/Desktop (please complete the following information):** 25 | - OS: [e.g. Windows 10, MacOS 10.14, Linux distro] 26 | - Browser: [e.g. Chrome, Safari] 27 | - Browser version: [e.g. 22] 28 | - Traverson Version: 29 | - traverson-angular Version: 30 | 31 | **Browser/Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, Safari, Chrome] 35 | - Browser Version [e.g. 22] 36 | - Traverson Version: 37 | - traverson-angular Version: 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem or a specific use case? Please describe.** 8 | A clear and concise description of what problem you are trying to solve or what you are trying to achieve and why. 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. Ideally, this includes a suggestion for any new API you would like to see added. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .node-version 3 | 4 | browser/dist/traverson-angular.external.js 5 | browser/dist/traverson-angular.external.min.js 6 | browser/dist/traverson-angular.js 7 | browser/dist/traverson-angular.min.js 8 | browser/test/browserified_tests.js 9 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npm test", 4 | "commit-msg": "commitlint -e" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "bitwise": true, 4 | "boss": true, 5 | "camelcase": true, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "evil": true, 9 | "expr": true, 10 | "forin": true, 11 | "immed": true, 12 | "indent": 2, 13 | "latedef": false, 14 | "laxcomma": true, 15 | "maxcomplexity": 6, 16 | "maxdepth": 4, 17 | "maxlen": 80, 18 | "maxparams": 5, 19 | "maxstatements": 300, 20 | "newcap": true, 21 | "noarg": true, 22 | "noempty": true, 23 | "nonew": true, 24 | "quotmark": true, 25 | "strict": false, 26 | "trailing": true, 27 | "undef": true, 28 | "node": true, 29 | "browser": true, 30 | "jquery": true, 31 | "globals": { 32 | "after": true, 33 | "afterEach": true, 34 | "before": true, 35 | "beforeEach": true, 36 | "describe": true, 37 | "define": true, 38 | "it": true, 39 | "mocha": true, 40 | "sinon": true, 41 | "traverson": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | *.swp 3 | .travis.yml 4 | disc.html 5 | test 6 | browser/test/browserified_tests.js 7 | browser/example 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '10.6.0' 5 | - '6' 6 | - '7' 7 | - '8' 8 | - '9' 9 | 10 | sudo: false 11 | 12 | env: 13 | global: 14 | secure: CvWvWn6pDIBUUdi3gVmvLSbgojqqG0NDhKQv9k6dg4U/1wUT0nhaiFsQAeCiFxCv6fclMA/oLhz4O6TUMBDXLUGXrKixu5Nk4kzrIthjbbukXoZ4K//3H/r9M3Zas21StS25dMszryhcvH5CbnO4DQqaV5cXlg9uyVru3k97q4U= 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ------------- 3 | 4 | * 0.12.0 2014-11-29: 5 | * Initial release 6 | * 0.13.0 2014-12-01 7 | * Reduce size of browser build by 33%. The minified version now has 37k instead of 55k (still too much, but also much better than before) 8 | * 0.14.0 2014-12-05: See [Traverson's release notes](https://github.com/traverson/traverson#release-notes) 9 | * 0.15.0 2014-12-06: See [Traverson's release notes](https://github.com/traverson/traverson#release-notes) 10 | * 1.0.0 2015-02-27: 11 | * Fixed humongous bug that only allowed GET requests but thwarted POST, PUT, PATCH and DELETE requests (#2 and #4) (thanks to @binarykitchen). 12 | * Traverson 1.0.0 contains a lot of changes, even some breaking changes regarding HAL. See [Traverson's release notes](https://github.com/traverson/traverson#release-notes). 13 | * 1.0.1 2015-03-02: 14 | * Use minification-proof array notation (#5, #6) (thanks to @jamiegaines) 15 | * 1.1.0 2015-03-03: 16 | * See [Traverson's release notes](https://github.com/traverson/traverson#release-notes) 17 | * The new feature to abort a link traversal process made it necessary to change the API: The action methods (`get`, `getResource`, `post`, ...) now return an object which has the property `result` which is the promise which had been returned directly until version 1.0.1. Thus, `getResource().then(...)` becomes `getResource().result.then(...)`. The old syntax `getResource().then(...)` still works for now, but is deprecated and will be removed in version 2.0.0. 18 | * 1.2.0 2015-03-15: 19 | * See [Traverson's release notes](https://github.com/traverson/traverson#release-notes) 20 | * The method `getUri` has been renamed to `getUrl`. `getUri` is now deprecated, but is kept as an alias for `getUrl`. 21 | * 1.2.1 2015-03-16: 22 | * Bugfix: fix `getUri` alias for `getUrl`. 23 | * 2.0.0 2015-04-08: 24 | * [Continue link traversals](#continuing-a-link-traversal) with `continue()` (also see [Traverson's docs](https://github.com/traverson/traverson#continuing-a-link-traversal) and [Traverson's API docs](https://github.com/traverson/traverson/blob/master/api.markdown#traversal-continue)). 25 | * The action methods (`get`, `getResource`, `post`, ...) now return an object which has the property `result` which is the promise which had been returned directly until version 1.0.1. Thus, `getResource().then(...)` becomes `getResource().result.then(...)`. The old syntax `getResource().then(...)` was deprecated in version 1.1.0 and has been removed with this version. 26 | * 2.1.0 2015-04-11 (using traverson 2.0.0): 27 | * Option to use AngularJS' $http service instead of Traverson's HTTP module. 28 | * 2.1.1 2015-04-30 (using traverson 2.0.0): 29 | * Allow chaining .useAngularHttp() method (thanks to @joshuajabbour) 30 | * 2.1.2 2015-05-04 (using traverson 2.0.1): 31 | * Update to Traverson 2.0.1, including a fix for issue [#11](https://github.com/traverson/traverson-angular/issues/11) (cloning a continued traversal (via `continue`) with `newRequest`). 32 | * 2.1.3 2015-05-07 (using traverson 2.0.1): 33 | * Enable projects that depend on traverson-angular to use angular-mocks to test traverson-angular related code ([#12](https://github.com/traverson/traverson-angular/issues/12), thanks to @meyertee) 34 | * 2.1.4 2015-08-27 (using traverson 2.1.0): 35 | * Update for Traverson release 2.1.0 (including `convertResponseToObject()`). 36 | * 3.0.0 2015-09-16: 37 | * Update for Traverson release 3.0.0 (including `followLocationHeader()`). 38 | * 3.1.0 2015-11-10: 39 | * Update to Traverson release 3.1.0 (including `withCredentials`). 40 | * 3.1.1 2015-12-21: 41 | * Update to Traverson release 3.1.1, including an update from JSONPath 0.10 to jsonpath-plus 0.13. (Fixes [#20](https://github.com/traverson/traverson-angular/issues/20).) 42 | * 5.0.0 2016-12-20: 43 | * Update to Traverson release 5.0.0. 44 | * See [Traverson's release notes](https://github.com/traverson/traverson/blob/master/CHANGELOG.md) 45 | * 6.0.0 2017-02-10: 46 | * Update to Traverson release 6.0.1 (including auto headers). 47 | * 6.0.1 2018-07-19: 48 | * Update to Traverson release 6.0.4. 49 | * 6.1.0 2018-09-10: 50 | * Update to Traverson release 6.1.0. 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Traverson Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at bastian.krol@web.de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to traverson-angular 2 | 3 | We'd love for you to contribute to our project and to make traverson-angular even better than it is today! Here are the guidelines we'd like you to follow: 4 | 5 | * [Code of Conduct](#coc) 6 | * [Questions and Problems](#question) 7 | * [Issues and Bugs](#issue) 8 | * [Feature Requests](#feature) 9 | * [Improving Documentation](#docs) 10 | * [Issue Submission Guidelines](#submit) 11 | * [Pull Request Submission Guidelines](#submit-pr) 12 | * [Code Style](#code-style) 13 | 14 | 15 | ## Code of Conduct 16 | 17 | Help us keep Traverson open and inclusive. Please read and follow our [code of conduct][coc]. 18 | 19 | ## Questions, Bugs, Features 20 | 21 | ### Got a Question or Problem? 22 | 23 | Feel free to open a GitHub issue for it, describing the problem and what you want to achieve as understandable as possible. Include comprehensive code snippets of what you tried and the error message you received or the unexpected behaviour you observed. 24 | 25 | ### Found an Issue or Bug? 26 | 27 | If you find a bug in the source code, you can help us by submitting an issue to our [GitHub repository][github]. Even better, you could also consider submitting a pull request with a fix. But just reporting the bug also already helps a lot, so that's fine, too. 28 | 29 | **Please see the [submission guidelines](#submit) below.** 30 | 31 | ### Want a Doc Fix? 32 | 33 | Should you have a suggestion for the documentation, you can open an issue and outline the problem or improvement you have or create the doc fix yourself. Please make sure that your commit message follows the **[commit message guidelines](#commits)**. 34 | 35 | ### Missing a Feature? 36 | 37 | You can request a new feature by submitting an issue to our [GitHub repository][github-issues]. 38 | 39 | If you would like to implement a new feature then consider what kind of change it is: 40 | 41 | * **Small Changes** can directly be crafted and submitted to the [GitHub repository][github] as a pull request. See the section about [pull request submission guidelines](#submit-pr) and the [coding guidelines](#coding-guidelines) in particular. 42 | * **Larger Changes** (like adding a completely new feature, changing the API, ...) that you wish to contribute to the project should be discussed first in a [GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature. 43 | 44 | ## Issue Submission Guidelines 45 | 46 | Before you submit your issue, search the archive, maybe your question was already answered. 47 | 48 | If your issue appears to be a bug and hasn't been reported, open a new issue. Help us to maximize the effort we can spend fixing issues and adding new features by not reporting duplicate issues. 49 | 50 | The [new issue][github-new-issue] template contains a number of prompts that you should fill out to make it easier to understand and categorize the issue. 51 | 52 | In general, providing the following information will increase the chances of your issue being dealt with quickly: 53 | 54 | * **Overview of the Issue** - if an error is being thrown, please include the full error message and a non-minified stack trace 55 | * **Motivation/Use Case** - explain why this is a bug for you 56 | * **Traverson/traverson-angular Version(s)** - is it a regression? 57 | * **Node.js version or Browser version** - is this a problem with all runtime environments (Node.js, Browser, React Native, whatever) or only specific ones? 58 | * **Reproduce the Error** - provide a code snippet or a live example (using [Plunker][plunker] or [JSFiddle][jsfiddle]) that reproduces the problem. See [SSCCE][sscce] for an awesome guideline for such reproducing examples. 59 | * **Related Issues** - has a similar issue been reported before? 60 | * **Suggest a Fix** (optional) - if you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit) 61 | 62 | Here are two examples of reasonably well defined issues: 63 | - https://github.com/traverson/traverson/issues/95 and 64 | - https://github.com/traverson/traverson/issues/82. 65 | 66 | ## Pull Request Submission Guidelines 67 | 68 | Before you submit your pull request, consider the following guidelines: 69 | 70 | * Search [GitHub][pull-requests] for an open or closed Pull Request that relates to your submission. You don't want to duplicate effort. 71 | * Seriously, let's talk before you put hours or days into an implementation. Especially if you plan a new feature or if your change includes an API change. Just open an issue, state what you plan to do, indicate that you are willing to put together a PR and ask for feedback. This will greatly enhance the probability to get your PR merged. Since we maintain Traverson in our free time, it might take a while to get a reaction sometimes. Be patient, we will respond eventually. If you desparately need a new feature *right now* to support your use case, feel free to implement it in a prototypcial way in a clone of this repository and use your [clone/branch in your package.json][npm-github-urls]. 72 | * Fork this repo, clone your fork, optionally create a dedicated branch for your changes. 73 | * Run `npm install` in your project directory. This will also install some pre-commit hooks. 74 | * Make sure to 75 | * Please add tests for all non-trivial changes. 76 | * Run `npm test` to run all tests and JSHint on the code. 77 | * If your changes touch the public API or add new API, please also need to add the corresponding documentation. 78 | * Create your commit(s), **including appropriate test cases**: 79 | * The commit message need to conform to our commit message conventions - all Traverson projects use the popular [Angular.js commit message conventions][angular-commit]. The commit message will also become the CHANGELOG entry, so please try to make sure it is comprehensible in the release notes context. Pre-commit hooks will check the commit message format. You can also use [commitizen][commitizen]. 80 | * In GitHub, send a pull request to `traverson-angular:master`. This will trigger the Travis integration. 81 | * If you find that the Travis integration has failed, look into the logs on Travis to find out if your changes caused test failures, the commit message was malformed etc. If you find that the tests failed or times out for unrelated reasons, you can ping a team member so that the build can be restarted. 82 | * If we suggest changes, then: 83 | * Make the required updates. 84 | * Re-run the test suite (`npm test`) to ensure tests are still passing. 85 | * Commit your changes to your branch or amend the initial commit. 86 | * Push the changes to your fork (this will update your pull request). 87 | 88 | That's it. Thank you for your contribution! 89 | 90 | ### Code Style 91 | 92 | Traverson does not have a comprehensive formal JavaScript code style guide. Please try to adhere to the existing code conventions. Here are a few code style rules: 93 | 94 | * Max line length: 80 characters 95 | * Indentation: 2 spaces, no tabs 96 | * Semicolons 97 | * ES5 compatible JavaScript 98 | * `'`, not `"` 99 | 100 | There is also an `.editorconfig` in the repository's top level, for editors that support it. 101 | 102 | 103 | 104 | 105 | Note: This guidelines have been partially inspired by the excellent [Angular.js contribution guidelines][angular-contrib-guide]. 106 | 107 | [angular-commit]: https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits 108 | [angular-contrib-guide]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md 109 | [coc]: https://github.com/traverson/traverson-angular/blob/master/CODE_OF_CONDUCT.md 110 | [commitizen]: https://github.com/commitizen/cz-cli 111 | [github-issues]: https://github.com/traverson/traverson-angular/issues 112 | [github-new-issue]: https://github.com/traverson/traverson-angular/issues/new/choose 113 | [github]: https://github.com/traverson/traverson-angular 114 | [jsfiddle]: http://jsfiddle.net/ 115 | [plunker]: http://plnkr.co/edit 116 | [pull-requests]: https://github.com/traverson/traverson-angular/pulls 117 | [sscce]: http://sscce.org 118 | [npm-github-urls]: https://docs.npmjs.com/files/package.json#github-urls 119 | 120 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request') 4 | , testServerRootUri = 'http://127.0.0.1:2808' 5 | , testServerStatusUri = testServerRootUri + '/status' 6 | , testServerKillUri = testServerRootUri + '/quit' 7 | , mochaPhantomJsTestRunner = testServerRootUri + 8 | '/static/browser/test/index.html' 9 | , serverWasAlreadyRunning = false; 10 | 11 | /* jshint -W106 */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.initConfig({ 15 | pkg: grunt.file.readJSON('package.json'), 16 | 17 | jshint: { 18 | files: [ 19 | '*.js', 20 | '.jshintrc', 21 | 'bin/*.js', 22 | 'test/**/*.js', 23 | 'browser/example/*.js', 24 | 'browser/example/browserify/example.js', 25 | ], 26 | options: { 27 | jshintrc: '.jshintrc' 28 | } 29 | }, 30 | 31 | // remove all previous browserified builds 32 | clean: { 33 | dist: ['./browser/dist/**/*.js'], 34 | tests: ['./browser/test/browserified_tests.js'] 35 | }, 36 | 37 | // browserify everything 38 | browserify: { 39 | // This browserify build can be used by users of the module. It contains 40 | // traverson and traverson-angular with all dependencies. A UMD (universal 41 | // module definition) and can be used via an AMD module loader like 42 | // RequireJS or by simply placing a script tag in the page, which 43 | // registers the module as a global var. Look at the example in in 44 | // browser/example/index.html. 45 | standalone: { 46 | src: [ '<%= pkg.name %>.js' ], 47 | dest: './browser/dist/<%= pkg.name %>.js', 48 | options: { 49 | browserifyOptions: { 50 | standalone: '<%= pkg.name %>' 51 | } 52 | } 53 | }, 54 | // With this browserify build, traverson-angular can be required by other 55 | // browserify modules that have been created with an --external parameter. 56 | // See browser/test/index.html for an example. 57 | external: { 58 | src: [ '<%= pkg.name %>.js' ], 59 | dest: './browser/dist/<%= pkg.name %>.external.js', 60 | options: { 61 | alias: [ './<%= pkg.name %>.js:traverson-angular' ] 62 | } 63 | }, 64 | // Browserify the tests 65 | tests: { 66 | src: [ 'test/browser_suite.js' ], 67 | dest: './browser/test/browserified_tests.js', 68 | options: { 69 | external: [ './<%= pkg.name %>.js:traverson-angular' ], 70 | browserifyOptions: { 71 | // Embed source map for tests 72 | debug: true 73 | } 74 | } 75 | } 76 | }, 77 | 78 | // Uglify browser libs 79 | uglify: { 80 | dist: { 81 | files: { 82 | 'browser/dist/<%= pkg.name %>.min.js': 83 | ['<%= browserify.standalone.dest %>'], 84 | 'browser/dist/<%= pkg.name %>.external.min.js': 85 | ['<%= browserify.external.dest %>'] 86 | } 87 | } 88 | }, 89 | 90 | mocha: { 91 | test: { 92 | options: { 93 | urls: [ mochaPhantomJsTestRunner ], 94 | timeout: 20000, 95 | reporter: 'spec', 96 | } 97 | } 98 | }, 99 | 100 | watch: { 101 | files: ['<%= jshint.files %>'], 102 | tasks: ['default'] 103 | }, 104 | }); 105 | 106 | // load all grunt-tasks 107 | require('load-grunt-tasks')(grunt); 108 | 109 | grunt.registerTask('start-test-server', 'Start the test server.', 110 | function() { 111 | var done = this.async(); 112 | 113 | function pingTestServer(callback) { 114 | request.get(testServerRootUri, function(error, response) { 115 | if (error) { 116 | callback(error); 117 | } else if (response.statusCode === 200) { 118 | callback(); 119 | } else { 120 | callback(new Error('HTTP status code was not 200 (as expected), ' + 121 | 'but ' + response.statusCode)); 122 | } 123 | }); 124 | } 125 | 126 | grunt.log.writeln('Starting test server from grunt.'); 127 | pingTestServer(function(error) { 128 | // Only start a test server instance if none is running. Rationale: 129 | // If an instance is running via supervisor while watching changed files, 130 | // we do not need to (and can not due to port conflicts) start a second 131 | // instance. 132 | if (error) { 133 | if (error.message !== 'connect ECONNREFUSED') { 134 | grunt.log.writeln('(Message from ping was: ' + error.message + ')'); 135 | } 136 | grunt.log.writeln('It seems the test server is currently not ' + 137 | 'running, will start a new instance to run mocha-phantomjs tests.'); 138 | require('./bin/start-test-server'); 139 | done(); 140 | } else { 141 | serverWasAlreadyRunning = true; 142 | grunt.log.writeln('Test server is already running.'); 143 | done(); 144 | } 145 | }); 146 | }); 147 | 148 | grunt.registerTask('stop-test-server', 'Stops the test server.', 149 | function() { 150 | var done = this.async(); 151 | if (serverWasAlreadyRunning) { 152 | grunt.log.writeln('Server was already running when Grunt build started,' + 153 | ' thus it will not be shut down now from Grunt.'); 154 | return done(); 155 | } else { 156 | grunt.log.writeln('Stopping test server from grunt.'); 157 | } 158 | request.get(testServerKillUri, function(error, response) { 159 | if (error) { 160 | if (error.message !== 'connect ECONNREFUSED') { 161 | grunt.log.writeln('(Message from stop request was: ' + error.message + 162 | ')'); 163 | } 164 | grunt.log.writeln('It seems the test server is not running at all, ' + 165 | 'doing nothing'); 166 | return done(); 167 | } else { 168 | grunt.log.writeln('Shutdown request has been send to test server, ' + 169 | 'test server should have been shut down.'); 170 | grunt.log.writeln(''); 171 | return done(); 172 | } 173 | }); 174 | }); 175 | 176 | grunt.registerTask('default', [ 177 | 'jshint', 178 | 'clean', 179 | 'browserify', 180 | 'uglify', 181 | 'start-test-server', 182 | 'mocha', 183 | 'stop-test-server' 184 | ]); 185 | }; 186 | /* jshint +W106 */ 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Bastian Krol 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /bin/browserify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script to browserify without Grunt - usually the Grunt build is used to 4 | # browserify and build everything. 5 | 6 | bin_path=`dirname $0` 7 | pushd $bin_path/.. > /dev/null 8 | 9 | browserify_cmd=node_modules/.bin/browserify 10 | 11 | # This browserify build can be used by users of the module. It contains a 12 | # UMD (universal module definition) and can be used via an AMD module 13 | # loader like RequireJS or by simply placing a script tag in the page, 14 | # which registers mymodule as a global var. You can see an example 15 | # in browser/example/index.html. 16 | $browserify_cmd \ 17 | --entry traverson-angular.js \ 18 | --outfile browser/dist/traverson-angular.js \ 19 | --standalone traverson-angular 20 | 21 | # This browserify build can be required by other browserify modules that 22 | # have been created with an --external parameter. browser/test/index.html uses 23 | # this. 24 | $browserify_cmd \ 25 | --entry traverson-angular.js \ 26 | --outfile browser/dist/traverson-angular.external.js \ 27 | --require ./traverson-angular 28 | 29 | # These are the browserified tests. 30 | $browserify_cmd \ 31 | --entry test/browser_suite.js \ 32 | --outfile browser/test/browserified_tests.js \ 33 | --external ./traverson-angular.js 34 | 35 | popd > /dev/null 36 | -------------------------------------------------------------------------------- /bin/start-test-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | , testServer = require('traverson-test-server'); 5 | 6 | // serve files from the root folder of the project 7 | testServer.serveStatic(path.join(__dirname, '..')); 8 | testServer.start(); 9 | -------------------------------------------------------------------------------- /bin/watchify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script assumes that watchify is installed globally. To do that execute 4 | # npm install -g watchify 5 | 6 | # Three watchify processes are started in the background. Use 7 | # pkill -f watchify or pkill -f "node.*watchify" 8 | # to stop them. 9 | 10 | bin_path=`dirname $0` 11 | pushd $bin_path/.. > /dev/null 12 | 13 | watchify \ 14 | --entry traverson-angular.js \ 15 | --outfile browser/dist/traverson-angular.js \ 16 | --standalone traverson-angular \ 17 | --debug \ 18 | --verbose \ 19 | & 20 | 21 | watchify \ 22 | --entry traverson-angular.js \ 23 | --outfile browser/dist/traverson-angular.external.js \ 24 | --require ./traverson-angular \ 25 | --debug \ 26 | --verbose \ 27 | & 28 | 29 | watchify \ 30 | --entry test/browser_suite.js \ 31 | --outfile browser/test/browserified_tests.js \ 32 | --external ./traverson-angular.js \ 33 | --debug \ 34 | --verbose \ 35 | & 36 | 37 | popd > /dev/null 38 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "traverson-angular", 3 | "main": "browser/dist/traverson-angular.js", 4 | "version": "6.1.0", 5 | "homepage": "https://github.com/traverson/traverson-angular", 6 | "authors": [ 7 | "Bastian Krol " 8 | ], 9 | "description": "AngularJS integration for Traverson, the hypermedia API/HATEOAS client for Node.js and the browser", 10 | "moduleType": [ 11 | "amd" 12 | ], 13 | "keywords": [ 14 | "AngularJS", 15 | "Traverson", 16 | "JSON", 17 | "REST", 18 | "API", 19 | "HATEOAS", 20 | "hypertext", 21 | "hypermedia", 22 | "HAL" 23 | ], 24 | "license": "MIT", 25 | "ignore": [ 26 | "**/.*", 27 | "bin", 28 | "bower_components", 29 | "howto_release.markdown", 30 | "node_modules", 31 | "server", 32 | "test" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /browser/example/README.md: -------------------------------------------------------------------------------- 1 | This folder contains an example html page that shows how Traverson works with AngularJS via traverson-angular. You need to start the test server with `node bin/start-test-server.js`, which serves index.html and the static assets as well as the REST API that is used in the example. Then open the URL `http://localhost:2808/static/browser/example/index.html` in your browser and click on the "Start Request" buttons to see traverson-angular in action (hint: keep an eye on the network tab in the dev tools to see what's going on). 2 | -------------------------------------------------------------------------------- /browser/example/assets/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.3.4 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | var $sanitizeMinErr = angular.$$minErr('$sanitize'); 9 | 10 | /** 11 | * @ngdoc module 12 | * @name ngSanitize 13 | * @description 14 | * 15 | * # ngSanitize 16 | * 17 | * The `ngSanitize` module provides functionality to sanitize HTML. 18 | * 19 | * 20 | *
21 | * 22 | * See {@link ngSanitize.$sanitize `$sanitize`} for usage. 23 | */ 24 | 25 | /* 26 | * HTML Parser By Misko Hevery (misko@hevery.com) 27 | * based on: HTML Parser By John Resig (ejohn.org) 28 | * Original code by Erik Arvidsson, Mozilla Public License 29 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 30 | * 31 | * // Use like so: 32 | * htmlParser(htmlString, { 33 | * start: function(tag, attrs, unary) {}, 34 | * end: function(tag) {}, 35 | * chars: function(text) {}, 36 | * comment: function(text) {} 37 | * }); 38 | * 39 | */ 40 | 41 | 42 | /** 43 | * @ngdoc service 44 | * @name $sanitize 45 | * @kind function 46 | * 47 | * @description 48 | * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are 49 | * then serialized back to properly escaped html string. This means that no unsafe input can make 50 | * it into the returned string, however, since our parser is more strict than a typical browser 51 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 52 | * browser, won't make it through the sanitizer. The input may also contain SVG markup. 53 | * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and 54 | * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. 55 | * 56 | * @param {string} html HTML input. 57 | * @returns {string} Sanitized HTML. 58 | * 59 | * @example 60 | 61 | 62 | 74 |
75 | Snippet: 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value 93 |
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
 94 | </div>
95 |
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
105 |
106 |
107 | 108 | it('should sanitize the html snippet by default', function() { 109 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 110 | toBe('

an html\nclick here\nsnippet

'); 111 | }); 112 | 113 | it('should inline raw snippet if bound to a trusted value', function() { 114 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). 115 | toBe("

an html\n" + 116 | "click here\n" + 117 | "snippet

"); 118 | }); 119 | 120 | it('should escape snippet without any filter', function() { 121 | expect(element(by.css('#bind-default div')).getInnerHtml()). 122 | toBe("<p style=\"color:blue\">an html\n" + 123 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 124 | "snippet</p>"); 125 | }); 126 | 127 | it('should update', function() { 128 | element(by.model('snippet')).clear(); 129 | element(by.model('snippet')).sendKeys('new text'); 130 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 131 | toBe('new text'); 132 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( 133 | 'new text'); 134 | expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( 135 | "new <b onclick=\"alert(1)\">text</b>"); 136 | }); 137 |
138 |
139 | */ 140 | function $SanitizeProvider() { 141 | this.$get = ['$$sanitizeUri', function($$sanitizeUri) { 142 | return function(html) { 143 | var buf = []; 144 | htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { 145 | return !/^unsafe/.test($$sanitizeUri(uri, isImage)); 146 | })); 147 | return buf.join(''); 148 | }; 149 | }]; 150 | } 151 | 152 | function sanitizeText(chars) { 153 | var buf = []; 154 | var writer = htmlSanitizeWriter(buf, angular.noop); 155 | writer.chars(chars); 156 | return buf.join(''); 157 | } 158 | 159 | 160 | // Regular Expressions for parsing tags and attributes 161 | var START_TAG_REGEXP = 162 | /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, 163 | END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, 164 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 165 | BEGIN_TAG_REGEXP = /^/g, 168 | DOCTYPE_REGEXP = /]*?)>/i, 169 | CDATA_REGEXP = //g, 170 | SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 171 | // Match everything outside of normal chars and " (quote character) 172 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; 173 | 174 | 175 | // Good source of info about elements and attributes 176 | // http://dev.w3.org/html5/spec/Overview.html#semantics 177 | // http://simon.html5.org/html-elements 178 | 179 | // Safe Void Elements - HTML5 180 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 181 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 182 | 183 | // Elements that you can, intentionally, leave open (and which close themselves) 184 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 185 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 186 | optionalEndTagInlineElements = makeMap("rp,rt"), 187 | optionalEndTagElements = angular.extend({}, 188 | optionalEndTagInlineElements, 189 | optionalEndTagBlockElements); 190 | 191 | // Safe Block Elements - HTML5 192 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + 193 | "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + 194 | "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 195 | 196 | // Inline Elements - HTML5 197 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + 198 | "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + 199 | "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); 200 | 201 | // SVG Elements 202 | // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements 203 | var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + 204 | "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + 205 | "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + 206 | "stop,svg,switch,text,title,tspan,use"); 207 | 208 | // Special Elements (can contain anything) 209 | var specialElements = makeMap("script,style"); 210 | 211 | var validElements = angular.extend({}, 212 | voidElements, 213 | blockElements, 214 | inlineElements, 215 | optionalEndTagElements, 216 | svgElements); 217 | 218 | //Attributes that have href and hence need to be sanitized 219 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); 220 | 221 | var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + 222 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + 223 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + 224 | 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + 225 | 'valign,value,vspace,width'); 226 | 227 | // SVG attributes (without "id" and "name" attributes) 228 | // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes 229 | var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + 230 | 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + 231 | 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + 232 | 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + 233 | 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + 234 | 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + 235 | 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + 236 | 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + 237 | 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + 238 | 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + 239 | 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + 240 | 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + 241 | 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + 242 | 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + 243 | 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + 244 | 'zoomAndPan'); 245 | 246 | var validAttrs = angular.extend({}, 247 | uriAttrs, 248 | svgAttrs, 249 | htmlAttrs); 250 | 251 | function makeMap(str) { 252 | var obj = {}, items = str.split(','), i; 253 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 254 | return obj; 255 | } 256 | 257 | 258 | /** 259 | * @example 260 | * htmlParser(htmlString, { 261 | * start: function(tag, attrs, unary) {}, 262 | * end: function(tag) {}, 263 | * chars: function(text) {}, 264 | * comment: function(text) {} 265 | * }); 266 | * 267 | * @param {string} html string 268 | * @param {object} handler 269 | */ 270 | function htmlParser(html, handler) { 271 | if (typeof html !== 'string') { 272 | if (html === null || typeof html === 'undefined') { 273 | html = ''; 274 | } else { 275 | html = '' + html; 276 | } 277 | } 278 | var index, chars, match, stack = [], last = html, text; 279 | stack.last = function() { return stack[ stack.length - 1 ]; }; 280 | 281 | while (html) { 282 | text = ''; 283 | chars = true; 284 | 285 | // Make sure we're not in a script or style element 286 | if (!stack.last() || !specialElements[ stack.last() ]) { 287 | 288 | // Comment 289 | if (html.indexOf("", index) === index) { 294 | if (handler.comment) handler.comment(html.substring(4, index)); 295 | html = html.substring(index + 3); 296 | chars = false; 297 | } 298 | // DOCTYPE 299 | } else if (DOCTYPE_REGEXP.test(html)) { 300 | match = html.match(DOCTYPE_REGEXP); 301 | 302 | if (match) { 303 | html = html.replace(match[0], ''); 304 | chars = false; 305 | } 306 | // end tag 307 | } else if (BEGING_END_TAGE_REGEXP.test(html)) { 308 | match = html.match(END_TAG_REGEXP); 309 | 310 | if (match) { 311 | html = html.substring(match[0].length); 312 | match[0].replace(END_TAG_REGEXP, parseEndTag); 313 | chars = false; 314 | } 315 | 316 | // start tag 317 | } else if (BEGIN_TAG_REGEXP.test(html)) { 318 | match = html.match(START_TAG_REGEXP); 319 | 320 | if (match) { 321 | // We only have a valid start-tag if there is a '>'. 322 | if (match[4]) { 323 | html = html.substring(match[0].length); 324 | match[0].replace(START_TAG_REGEXP, parseStartTag); 325 | } 326 | chars = false; 327 | } else { 328 | // no ending tag found --- this piece should be encoded as an entity. 329 | text += '<'; 330 | html = html.substring(1); 331 | } 332 | } 333 | 334 | if (chars) { 335 | index = html.indexOf("<"); 336 | 337 | text += index < 0 ? html : html.substring(0, index); 338 | html = index < 0 ? "" : html.substring(index); 339 | 340 | if (handler.chars) handler.chars(decodeEntities(text)); 341 | } 342 | 343 | } else { 344 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), 345 | function(all, text) { 346 | text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); 347 | 348 | if (handler.chars) handler.chars(decodeEntities(text)); 349 | 350 | return ""; 351 | }); 352 | 353 | parseEndTag("", stack.last()); 354 | } 355 | 356 | if (html == last) { 357 | throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + 358 | "of html: {0}", html); 359 | } 360 | last = html; 361 | } 362 | 363 | // Clean up any remaining tags 364 | parseEndTag(); 365 | 366 | function parseStartTag(tag, tagName, rest, unary) { 367 | tagName = angular.lowercase(tagName); 368 | if (blockElements[ tagName ]) { 369 | while (stack.last() && inlineElements[ stack.last() ]) { 370 | parseEndTag("", stack.last()); 371 | } 372 | } 373 | 374 | if (optionalEndTagElements[ tagName ] && stack.last() == tagName) { 375 | parseEndTag("", tagName); 376 | } 377 | 378 | unary = voidElements[ tagName ] || !!unary; 379 | 380 | if (!unary) 381 | stack.push(tagName); 382 | 383 | var attrs = {}; 384 | 385 | rest.replace(ATTR_REGEXP, 386 | function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { 387 | var value = doubleQuotedValue 388 | || singleQuotedValue 389 | || unquotedValue 390 | || ''; 391 | 392 | attrs[name] = decodeEntities(value); 393 | }); 394 | if (handler.start) handler.start(tagName, attrs, unary); 395 | } 396 | 397 | function parseEndTag(tag, tagName) { 398 | var pos = 0, i; 399 | tagName = angular.lowercase(tagName); 400 | if (tagName) 401 | // Find the closest opened tag of the same type 402 | for (pos = stack.length - 1; pos >= 0; pos--) 403 | if (stack[ pos ] == tagName) 404 | break; 405 | 406 | if (pos >= 0) { 407 | // Close all the open elements, up the stack 408 | for (i = stack.length - 1; i >= pos; i--) 409 | if (handler.end) handler.end(stack[ i ]); 410 | 411 | // Remove the open elements from the stack 412 | stack.length = pos; 413 | } 414 | } 415 | } 416 | 417 | var hiddenPre=document.createElement("pre"); 418 | var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; 419 | /** 420 | * decodes all entities into regular string 421 | * @param value 422 | * @returns {string} A string with decoded entities. 423 | */ 424 | function decodeEntities(value) { 425 | if (!value) { return ''; } 426 | 427 | // Note: IE8 does not preserve spaces at the start/end of innerHTML 428 | // so we must capture them and reattach them afterward 429 | var parts = spaceRe.exec(value); 430 | var spaceBefore = parts[1]; 431 | var spaceAfter = parts[3]; 432 | var content = parts[2]; 433 | if (content) { 434 | hiddenPre.innerHTML=content.replace(//g, '>'); 465 | } 466 | 467 | /** 468 | * create an HTML/XML writer which writes to buffer 469 | * @param {Array} buf use buf.jain('') to get out sanitized html string 470 | * @returns {object} in the form of { 471 | * start: function(tag, attrs, unary) {}, 472 | * end: function(tag) {}, 473 | * chars: function(text) {}, 474 | * comment: function(text) {} 475 | * } 476 | */ 477 | function htmlSanitizeWriter(buf, uriValidator) { 478 | var ignore = false; 479 | var out = angular.bind(buf, buf.push); 480 | return { 481 | start: function(tag, attrs, unary) { 482 | tag = angular.lowercase(tag); 483 | if (!ignore && specialElements[tag]) { 484 | ignore = tag; 485 | } 486 | if (!ignore && validElements[tag] === true) { 487 | out('<'); 488 | out(tag); 489 | angular.forEach(attrs, function(value, key) { 490 | var lkey=angular.lowercase(key); 491 | var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); 492 | if (validAttrs[lkey] === true && 493 | (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { 494 | out(' '); 495 | out(key); 496 | out('="'); 497 | out(encodeEntities(value)); 498 | out('"'); 499 | } 500 | }); 501 | out(unary ? '/>' : '>'); 502 | } 503 | }, 504 | end: function(tag) { 505 | tag = angular.lowercase(tag); 506 | if (!ignore && validElements[tag] === true) { 507 | out(''); 510 | } 511 | if (tag == ignore) { 512 | ignore = false; 513 | } 514 | }, 515 | chars: function(chars) { 516 | if (!ignore) { 517 | out(encodeEntities(chars)); 518 | } 519 | } 520 | }; 521 | } 522 | 523 | 524 | // define ngSanitize module and register $sanitize service 525 | angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); 526 | 527 | /* global sanitizeText: false */ 528 | 529 | /** 530 | * @ngdoc filter 531 | * @name linky 532 | * @kind function 533 | * 534 | * @description 535 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 536 | * plain email address links. 537 | * 538 | * Requires the {@link ngSanitize `ngSanitize`} module to be installed. 539 | * 540 | * @param {string} text Input text. 541 | * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. 542 | * @returns {string} Html-linkified text. 543 | * 544 | * @usage 545 | 546 | * 547 | * @example 548 | 549 | 550 | 562 |
563 | Snippet: 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 575 | 578 | 579 | 580 | 581 | 584 | 587 | 588 | 589 | 590 | 591 | 592 | 593 |
FilterSourceRendered
linky filter 573 |
<div ng-bind-html="snippet | linky">
</div>
574 |
576 |
577 |
linky target 582 |
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
583 |
585 |
586 |
no filter
<div ng-bind="snippet">
</div>
594 | 595 | 596 | it('should linkify the snippet with urls', function() { 597 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 598 | toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + 599 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 600 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); 601 | }); 602 | 603 | it('should not linkify snippet without the linky filter', function() { 604 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). 605 | toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + 606 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 607 | expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); 608 | }); 609 | 610 | it('should update', function() { 611 | element(by.model('snippet')).clear(); 612 | element(by.model('snippet')).sendKeys('new http://link.'); 613 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 614 | toBe('new http://link.'); 615 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); 616 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) 617 | .toBe('new http://link.'); 618 | }); 619 | 620 | it('should work with the target property', function() { 621 | expect(element(by.id('linky-target')). 622 | element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). 623 | toBe('http://angularjs.org/'); 624 | expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); 625 | }); 626 | 627 | 628 | */ 629 | angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { 630 | var LINKY_URL_REGEXP = 631 | /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/, 632 | MAILTO_REGEXP = /^mailto:/; 633 | 634 | return function(text, target) { 635 | if (!text) return text; 636 | var match; 637 | var raw = text; 638 | var html = []; 639 | var url; 640 | var i; 641 | while ((match = raw.match(LINKY_URL_REGEXP))) { 642 | // We can not end in these as they are sometimes found at the end of the sentence 643 | url = match[0]; 644 | // if we did not match ftp/http/mailto then assume mailto 645 | if (match[2] == match[3]) url = 'mailto:' + url; 646 | i = match.index; 647 | addText(raw.substr(0, i)); 648 | addLink(url, match[0].replace(MAILTO_REGEXP, '')); 649 | raw = raw.substring(i + match[0].length); 650 | } 651 | addText(raw); 652 | return $sanitize(html.join('')); 653 | 654 | function addText(text) { 655 | if (!text) { 656 | return; 657 | } 658 | html.push(sanitizeText(text)); 659 | } 660 | 661 | function addLink(url, text) { 662 | html.push(''); 671 | addText(text); 672 | html.push(''); 673 | } 674 | }; 675 | }]); 676 | 677 | 678 | })(window, window.angular); 679 | -------------------------------------------------------------------------------- /browser/example/assets/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traverson/traverson-angular/4ebad59b334d91ccf0ff831b02cc6e53167abc6d/browser/example/assets/spinner.gif -------------------------------------------------------------------------------- /browser/example/assets/traverson-hal.min.js: -------------------------------------------------------------------------------- 1 | !function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;"undefined"!=typeof window?b=window:"undefined"!=typeof global?b=global:"undefined"!=typeof self&&(b=self),b.TraversonJsonHalAdapter=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g1&&c.warn("Found HAL link array with more than one element for key "+a.parsedKey.key+", arbitrarily choosing index "+e+", because it was the first that had a href attribute."),c.debug("found hal link: "+d.href),a.linkStep={url:d.href})}function l(a,b){b.debug("checking for embedded: "+a.parsedKey.key+(a.parsedKey.index?a.parsedKey.index:""));var c=a.halResource.embeddedArray(a.parsedKey.key);if((!c||0===c.length)&&"all"!==a.parsedKey.mode)return null;switch(b.debug("Found an array of embedded resource for: "+a.parsedKey.key),a.parsedKey.mode){case"secondary":m(a,c,b);break;case"index":n(a,c,b);break;case"all":o(a,c,b);break;case"first":p(a,c,b);break;default:throw new Error("Illegal mode: "+a.parsedKey.mode)}}function m(a,b,c){for(var d=0;d1&&c.warn("Found HAL embedded resource array with more than one element for key "+a.parsedKey.key+", arbitrarily choosing first element."),a.embeddedStep={doc:b[0].original()}}var q=a("halfred");d.mediaType="application/hal+json",d.prototype.findNextStep=function(a,b){if("undefined"==typeof b||null===b)throw new Error("Link object is null or undefined.");if("object"!=typeof b)throw new Error("Links must be objects, not "+typeof b+": ",b);if(!b.type)throw new Error("Link objects has no type attribute.",b);switch(b.type){case"link-rel":return this._handleLinkRel(a,b);case"header":return this._handleHeader(a.lastStep.response,b);default:throw new Error("Link objects with type "+b.type+" are not supported by this adapter.",b)}},d.prototype._handleLinkRel=function(a,b){var c=a.lastStep.doc,d=b.value,i=a.preferEmbedded;this.log.debug("parsing hal");var j={doc:c,halResource:q.parse(c),parsedKey:f(d),linkStep:null,embeddedStep:null};return g(j),h(j,this.log),l(j,this.log),e(j,d,i)},d.prototype._handleHeader=function(a,b){switch(b.value){case"location":var c=a.headers.location;if(!c)throw new Error("Following the location header but there was no location header in the last response.");return{url:c};default:throw new Error("Link objects with type header and value "+b.value+" are not supported by this adapter.",b)}},b.exports=d},{halfred:2}],2:[function(a,b,c){var d=a("./lib/parser"),e=!1;b.exports={parse:function(a){return(new d).parse(a,e)},enableValidation:function(a){e=null!=a?a:!0},disableValidation:function(){e=!1}}},{"./lib/parser":4}],3:[function(a,b,c){"use strict";function d(){arguments.length>=1?this._array=arguments[0]:this._array=[]}d.prototype.array=function(){return this._array},d.prototype.isEmpty=function(a){return 0===this._array.length},d.prototype.push=function(a){var b=this._array.slice(0);return b.push(a),new d(b)},d.prototype.pop=function(){var a=this._array.slice(0,this._array.length-1);return new d(a)},d.prototype.peek=function(){if(this.isEmpty())throw new Error("can't peek on empty stack");return this._array[this._array.length-1]},b.exports=d},{}],4:[function(a,b,c){"use strict";function d(){}function e(a,b,c){if(null==a)return a;var d=f(a._links,b,c.push("_links")),e=g(d),j=h(a._embedded,b,c.push("_embedded")),k=new t(d,e,j,b);return i(a,k),k._original=a,k}function f(a,b,c){return a=j(a,l,b,c),(null==a||null==a.self)&&p("Resource does not have a self link",b,c),a}function g(a){return a?a.curies:[]}function h(a,b,c){var d=j(a,o,b,c);return null==d?d:(Object.keys(d).forEach(function(a){d[a]=d[a].map(function(d){var f=null!=b?[]:null,g=e(d,f,c.push(a));return g._original=d,g})}),d)}function i(a,b){Object.keys(a).forEach(function(c){"_links"!==c&&"_embedded"!==c&&(b[c]=a[c])})}function j(a,b,c,d){if(null==a)return a;var e={};return Object.keys(a).forEach(function(f){e[f]=k(f,a[f],b,c,d)}),e}function k(a,b,c,d,e){return m(b)?b.map(function(b){return c(a,b,d,e)}):[c(a,b,d,e)]}function l(a,b,c,d){if(!n(b))throw new Error("Link object is not an actual object: "+b+" ["+typeof b+"]");var e=r(b);return Object.keys(v).forEach(function(b){null==e[b]&&(v[b].required&&p("Link misses required property "+b+".",c,d.push(a)),null!=v[b].defaultValue&&(e[b]=v[b].defaultValue))}),e.deprecation&&q("Warning: Link "+s(d.push(a))+" is deprecated, see "+e.deprecation),e.templated!==!0&&e.templated!==!1&&(e.templated=!1),c?(e.href&&e.href.indexOf("{")>=0&&!e.templated&&p('Link seems to be an URI template but its "templated" property is not set to true.',c,d.push(a)),e):e}function m(a){return"[object Array]"===Object.prototype.toString.call(a)}function n(a){return"object"==typeof a}function o(a,b){return b}function p(a,b,c){b&&b.push({path:s(c),message:a})}function q(a){"undefined"!=typeof console&&"function"==typeof console.log&&console.log(a)}function r(a){var b={};return Object.keys(a).forEach(function(c){b[c]=a[c]}),b}function s(a){for(var b="$.",c=0;c=1?d[c]:null}d.prototype._initCuries=function(a){if(this._curiesMap={},a){this._curies=a;for(var b=0;b=1){var e=a.href.replace(/(.*){(.*)}(.*)/,"$1"+c[1]+"$3");this._resolvedCuriesMap[e]=b}else this._resolvedCuriesMap[a.href]=b},d.prototype.allLinkArrays=function(){return this._links},d.prototype.linkArray=function(a){return e(this._links,a)},d.prototype.link=function(a,b){return f(this._links,a,b)},d.prototype.hasCuries=function(a){return this._curies.length>0},d.prototype.curieArray=function(a){return this._curies},d.prototype.curie=function(a){return this._curiesMap[a]},d.prototype.reverseResolveCurie=function(a){return this._resolvedCuriesMap[a]},d.prototype.allEmbeddedResourceArrays=function(){return this._embedded},d.prototype.embeddedResourceArray=function(a){return e(this._embedded,a)},d.prototype.embeddedResource=function(a,b){return f(this._embedded,a,b)},d.prototype.original=function(){return this._original},d.prototype.validationIssues=function(){return this._validation},d.prototype.allLinks=d.prototype.allLinkArrays,d.prototype.allEmbeddedArrays=d.prototype.allEmbeddedResources=d.prototype.allEmbeddedResourceArrays,d.prototype.embeddedArray=d.prototype.embeddedResourceArray,d.prototype.embedded=d.prototype.embeddedResource,d.prototype.validation=d.prototype.validationIssues,b.exports=d},{}]},{},[1])(1)}); -------------------------------------------------------------------------------- /browser/example/browserify/angular/angular-common-js.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | require('./angular.js'); 3 | 4 | module.exports = angular; 5 | -------------------------------------------------------------------------------- /browser/example/browserify/browserify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bin_path=`dirname $0` 4 | pushd $bin_path > /dev/null 5 | 6 | 7 | node_modules/.bin/browserify \ 8 | --entry example.js \ 9 | --outfile bundle.js 10 | 11 | popd > /dev/null 12 | -------------------------------------------------------------------------------- /browser/example/browserify/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var angular = require('angular'); 4 | require('angular-sanitize'); 5 | 6 | var traverson = require('traverson-angular'); 7 | 8 | var app = angular 9 | .module('traverson-angular-browserify-example', 10 | [traverson.name, 'ngSanitize']); 11 | 12 | var rootUri = '/'; 13 | 14 | app.controller('generalSetup', function($scope) { 15 | $scope.code = 16 | 'var rootUri = \'' + rootUri + '\';
' + 17 | 'var api = traverson.from(rootUri);

' + 18 | 19 | '// Requiring and registering the traverson-hal plug-in is fully ' + 20 | 'optional,
' + 21 | '// you only need that when you want HAL support.
' + 22 | 'var JsonHalAdapter = require(\'traverson-hal\');
' + 23 | 'traverson.registerMediaType(JsonHalAdapter.mediaType, ' + 24 | 'JsonHalAdapter);
'; 25 | }); 26 | 27 | app.service('apiService', function(traverson) { 28 | 29 | var api = traverson.from(rootUri); 30 | 31 | // Requiring and registering the traverson-hal plug-in is fully optional, 32 | // you only need that when you want HAL support. 33 | var JsonHalAdapter = require('traverson-hal'); 34 | traverson.registerMediaType(JsonHalAdapter.mediaType, JsonHalAdapter); 35 | 36 | this.plainVanilla = function() { 37 | return api 38 | .json() 39 | .withRequestOptions({ headers: { 'accept': 'application/json' } }) 40 | .follow('second', 'doc') 41 | .getResource(); 42 | }; 43 | 44 | this.jsonPath = function() { 45 | return api 46 | .json() 47 | .withRequestOptions({ headers: { 'accept': 'application/json' } }) 48 | .follow('$.jsonpath.nested.key') 49 | .getResource(); 50 | }; 51 | 52 | this.uriTemplate = function() { 53 | return api 54 | .json() 55 | .withRequestOptions({ headers: { 'accept': 'application/json' } }) 56 | .follow('uri_template') 57 | .withTemplateParameters({ param: 'foobar', id: 13 }) 58 | .getResource(); 59 | }; 60 | 61 | this.jsonHal = function() { 62 | return api 63 | .jsonHal() 64 | .withRequestOptions({ headers: { 'accept': 'application/hal+json' } }) 65 | .follow('first', 'second', 'inside_second') 66 | .getResource(); 67 | }; 68 | }); 69 | 70 | app.controller('plainVanillaController', function($scope, apiService) { 71 | 72 | $scope.start = function() { 73 | $scope.response = '... talking to server, please stand by ...'; 74 | apiService.plainVanilla().then(function(resource) { 75 | $scope.response = JSON.stringify(resource, null, 2); 76 | }, function(err) { 77 | $scope.response = err.message || JSON.stringify(err); 78 | }); 79 | }; 80 | 81 | $scope.code = 82 | 'api
' + 83 | '.json()
' + 84 | '.withRequestOptions({
' + 85 | ' headers: { \'accept\': \'application/json\' }
' + 86 | '})
' + 87 | '.follow(\'second\', \'doc\')
' + 88 | '.getResource()
' + 89 | '.then(function(resource) {
' + 90 | ' // do something with the resource...
' + 91 | '});'; 92 | }); 93 | 94 | app.controller('jsonPathController', function($scope, apiService) { 95 | $scope.start = function() { 96 | $scope.response = '... talking to server, please stand by ...'; 97 | apiService.jsonPath().then(function(resource) { 98 | $scope.response = JSON.stringify(resource, null, 2); 99 | }, function(err) { 100 | $scope.response = err.message || JSON.stringify(err); 101 | }); 102 | }; 103 | 104 | $scope.code = 105 | 'api
' + 106 | '.json()
' + 107 | '.withRequestOptions({
' + 108 | ' headers: { \'accept\': \'application/json\' }
' + 109 | '})
' + 110 | '.follow(\'$.jsonpath.nested.key\')
' + 111 | '.getResource()
' + 112 | '.then(function(resource) {
' + 113 | ' // do something with the resource...
' + 114 | '});'; 115 | }); 116 | 117 | app.controller('uriTemplateController', function($scope, apiService) { 118 | $scope.start = function() { 119 | $scope.response = '... talking to server, please stand by ...'; 120 | apiService.uriTemplate().then(function(resource) { 121 | $scope.response = JSON.stringify(resource, null, 2); 122 | }, function(err) { 123 | $scope.response = err.message || JSON.stringify(err); 124 | }); 125 | }; 126 | 127 | $scope.code = 128 | 'api
' + 129 | '.json()
' + 130 | '.withRequestOptions({
' + 131 | ' headers: { \'accept\': \'application/json\' }
' + 132 | '})
' + 133 | '.follow(\'uri_template\')
' + 134 | '.withTemplateParameters({param: \'foobar\', id: 13})
' + 135 | '.getResource()
' + 136 | '.then(function(resource) {
' + 137 | ' // do something with the resource...
' + 138 | '});'; 139 | }); 140 | 141 | app.controller('jsonHalController', function($scope, apiService) { 142 | $scope.start = function() { 143 | $scope.response = '... talking to server, please stand by ...'; 144 | // traverson-hal needs to be registered, see above. 145 | apiService.jsonHal().then(function(resource) { 146 | $scope.response = JSON.stringify(resource, null, 2); 147 | }, function(err) { 148 | $scope.response = err.message || JSON.stringify(err); 149 | }); 150 | }; 151 | 152 | $scope.code = 153 | '// traverson-hal needs to be registered, see above.
' + 154 | 'api
' + 155 | '.jsonHal()
' + 156 | '.withRequestOptions({
' + 157 | ' headers: { \'accept\': \'application/hal+json\' }
' + 158 | '})
' + 159 | '.follow(\'first\', \'second\', \'inside_second\')
' + 160 | '.getResource()
' + 161 | '.then(function(resource) {
' + 162 | ' // do something with the resource...
' + 163 | '});
'; 164 | }); 165 | -------------------------------------------------------------------------------- /browser/example/browserify/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | traverson-angular example — using traverson-angular with 5 | browserify 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Traverson-angular with Browserify

13 |

Traverson makes 14 | it easy to work with REST/HATEOAS APIs. It works in Node.js as well as 15 | in the browser. traverson-angular makes it easy to 16 | integrate Traverson into your AngularJS application. This example shows 17 | how to use traverson-angular with Browserify. It talks to the local 18 | Traverson test server.

19 |

There is also an example on how to use traverson-angular without 20 | browserify: Non-browserify Example and an 21 | example using the GitHub API with 22 | traverson-angular. 23 |

24 |
25 | 26 | 27 |

28 | You need to start the test server contained in the traverson-angular 29 | repository with node bin/start-test-server.js. This 30 | serves this page as well as the REST API that is used in the examples. 31 | Open the URL 32 | 33 | http://localhost:2808/static/browser/example/browserify/index.html 34 | in your browser. If the URL in your browser's address bar says 35 | file:... the examples on this page will not work. 36 |

37 |
38 | 39 |

Setup

40 |
41 | 42 |
43 | 44 |
45 | 46 |

Plain Vanilla Link Following

47 |
48 |
49 | 50 |
51 |
52 | Start Request 53 |
54 |

55 |       
56 |
57 | 58 |

JSONPath

59 |
60 |
61 | 62 |
63 |
64 | Start Request 65 |
66 |

67 |       
68 |
69 | 70 |

URI Templates

71 |
72 |
73 | 74 |
75 |
76 | Start Request 77 |
78 |

79 |       
80 |
81 | 82 |

HAL

83 |
84 |
85 | 86 |
87 |
88 | Start Request 89 |
90 |

91 |       
92 |
93 | 94 | < 95 | -------------------------------------------------------------------------------- /browser/example/browserify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "traverson-angular-browserify-example", 3 | "version": "1.0.0", 4 | "description": "Example how to use traverson-angular with Browserify", 5 | "main": "example.js", 6 | "author": "Bastian Krol ", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "postinstall": "./browserify.sh" 11 | }, 12 | "dependencies": { 13 | "angular": "^1.3.4", 14 | "angular-sanitize": "^1.3.4", 15 | "traverson-angular": "^6.0.0", 16 | "traverson-hal": "^6.0.2" 17 | }, 18 | "browser": { 19 | "angular": "./angular/angular-common-js.js" 20 | }, 21 | "devDependencies": { 22 | "browserify": "^16.2.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /browser/example/browserify/watchify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bin_path=`dirname $0` 4 | pushd $bin_path > /dev/null 5 | 6 | watchify \ 7 | --entry example.js \ 8 | --outfile bundle.js \ 9 | --debug \ 10 | --verbose 11 | & 12 | 13 | popd > /dev/null 14 | -------------------------------------------------------------------------------- /browser/example/github-example.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | (function() { 3 | 'use strict'; 4 | 5 | var app = 6 | angular.module('traverson-angular-github-example', 7 | ['traverson', 'ngSanitize']); 8 | 9 | var rootUri = 'https://api.github.com/'; 10 | 11 | app.controller('generalSetup', function($scope) { 12 | $scope.code = 13 | 'var rootUri = \'' + rootUri + '\';
' + 14 | 'var api = traverson.from(rootUri);
'; 15 | }); 16 | 17 | app.service('apiService', function(traverson) { 18 | var api = traverson.from(rootUri); 19 | this.commitComment = function() { 20 | return api 21 | .json() 22 | .follow('repository_url', 'commits_url', 'comments_url') 23 | .withTemplateParameters({ 24 | owner: 'traverson', 25 | repo: 'traverson', 26 | sha: '5c82c74583ee67eae727466179dd66c91592dd4a' 27 | }).getResource(); 28 | }; 29 | }); 30 | 31 | 32 | app.controller('commitCommentController', function($scope, apiService) { 33 | $scope.start = function() { 34 | $scope.response = '... talking to GitHub, please stand by ...'; 35 | apiService.commitComment().result.then(function(resource) { 36 | $scope.response = JSON.stringify(resource, null, 2); 37 | }, function(err) { 38 | $scope.response = err.message || JSON.stringify(err); 39 | }); 40 | }; 41 | 42 | $scope.code = 43 | 'api
' + 44 | '.json()
' + 45 | '.follow(\'repository_url\', \'commits_url\', \'comments_url\')
' + 46 | '.withTemplateParameters({
' + 47 | '  owner: \'traverson\',
' + 48 | '  repo: \'traverson\',
' + 49 | '  sha: \'5c82c74583ee67eae727466179dd66c91592dd4a\'
' + 50 | ')}.result.then(function(resource) {
' + 51 | '  // do something with the resource...
' + 52 | '});
'; 53 | }); 54 | })(); 55 | -------------------------------------------------------------------------------- /browser/example/github.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | traverson-angular example 5 | 6 | 7 | 8 |
9 |
10 |

traverson-angular

11 |

AngularJS adapter for Traverson

12 |

Talking to the GitHub API

13 |

Traverson makes 14 | it easy to work with REST/HATEOAS APIs. It works in Node.js as well as 15 | in the browser. traverson-angular makes it easy to 16 | integrate Traverson into your AngularJS application.This example talks 17 | to the GitHub API - from your browser, in an AngularJS app.

18 |

There is also the 19 | Traverson Test Server Example, an 20 | example using traverson-angular with 21 | Browserify and an example using HAL. 22 |

23 |
24 | 25 | 26 |

27 | To run this example locally, you should start the test server contained 28 | in the traverson-angular repository with 29 | node bin/start-test-server.js. 30 | This serves this page and its static assets. Then open the URL 31 | http://localhost:2808/static/browser/example/github.html 33 | in your browser. 34 |

35 |
36 | 37 |

Setup

38 |
39 | 40 |
41 | 42 |
43 | 44 |

Find a commit comment

45 |
46 |
47 | 48 |
49 |
50 | Start Request 51 |
52 |

53 |       
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /browser/example/hal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | traverson-angular HAL example 5 | 6 | 7 | 8 |
9 |
10 |

traverson-angular with HAL

11 |

AngularJS adapter for Traverson

12 |

Traverson makes 13 | it easy to work with REST/HATEOAS APIs. It works in Node.js as well as 14 | in the browser. traverson-angular makes it easy to 15 | integrate Traverson into your AngularJS application. This example 16 | demonstrates how to use traverson-angular together with media type 17 | plug-ins like traverson-hal. This example talks to the local Traverson 18 | test server.

19 |

There is also the 20 | more comprehensive example without HAL, an 21 | example using the GitHub API Example and 22 | an example using traverson-angular with 23 | Browserify. 24 |

25 |
26 | 27 | 28 |

29 | To run this example locally, you should start the test server contained 30 | in the traverson-angular repository with 31 | node bin/start-test-server.js. 32 | This serves this page as well as the REST API that is used in the 33 | examples. Open the URL 34 | http://localhost:2808/static/browser/example 36 | in your browser. If the URL in your browser's address bar says 37 | file:... the examples on this page will not work. 38 |

39 |
40 | 41 |

Setup

42 |
43 | 44 |
45 | 46 |
47 | 48 |

Working with HAL

49 |
50 |
51 | 52 |
53 |
54 | Start Request 55 |
56 |

57 |       
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /browser/example/hal.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | (function() { 4 | /* global TraversonJsonHalAdapter */ 5 | 'use strict'; 6 | 7 | var app = 8 | angular.module('traverson-angular-hal-example', 9 | ['traverson', 'ngSanitize']); 10 | 11 | var rootUri = '/'; 12 | 13 | app.controller('generalSetup', function($scope) { 14 | $scope.code = 15 | '// register HAL adapter in Traverson\'s media type registry
' + 16 | 'traverson
' + 17 | '.registerMediaType(TraversonJsonHalAdapter.mediaType,' + 18 | ' TraversonJsonHalAdapter);
' + 19 | '
' + 20 | 'var rootUri = \'' + rootUri + '\';
' + 21 | 'var api = traverson
' + 22 | '.from(rootUri)
' + 23 | '.jsonHal()
' + 24 | '.withRequestOptions({ headers: { ' + 25 | '\'accept\': \'application/hal+json\' } });'; 26 | }); 27 | 28 | app.service('apiService', function(traverson) { 29 | 30 | // register HAL adapter in Traverson's media type registry 31 | traverson 32 | .registerMediaType(TraversonJsonHalAdapter.mediaType, 33 | TraversonJsonHalAdapter); 34 | 35 | var api = traverson 36 | .from(rootUri) 37 | .jsonHal() 38 | .withRequestOptions({ headers: { 'accept': 'application/hal+json' } }); 39 | 40 | this.hal = function() { 41 | return api 42 | .follow('first', 'second', 'inside_second') 43 | .getResource(); 44 | }; 45 | 46 | }); 47 | 48 | app.controller('halController', function($scope, apiService) { 49 | $scope.start = function() { 50 | $scope.response = '... talking to server, please stand by ...'; 51 | apiService.hal().result.then(function(resource) { 52 | $scope.response = JSON.stringify(resource, null, 2); 53 | }, function(err) { 54 | $scope.response = err.message || JSON.stringify(err); 55 | }); 56 | }; 57 | 58 | $scope.code = 59 | 'api
' + 60 | '.follow(\'first\', \'second\', \'inside_second\')
' + 61 | '.getResource()
' + 62 | '.result
' + 63 | '.then(function(resource) {
' + 64 | ' // do something with the resource...
' + 65 | '});'; 66 | }); 67 | 68 | })(); 69 | -------------------------------------------------------------------------------- /browser/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | traverson-angular example 5 | 6 | 7 | 8 |
9 |
10 |

traverson-angular

11 |

AngularJS adapter for Traverson

12 |

Traverson makes 13 | it easy to work with REST/HATEOAS APIs. It works in Node.js as well as 14 | in the browser. traverson-angular makes it easy to 15 | integrate Traverson into your AngularJS application. This example talks 16 | to the local Traverson test server.

17 |

There is also the GitHub API Example, the 18 | HAL example and an 19 | example using traverson-angular with 20 | Browserify. 21 |

22 |
23 | 24 | 25 |

26 | To run this example locally, you should start the test server contained 27 | in the traverson-angular repository with 28 | node bin/start-test-server.js. 29 | This serves this page as well as the REST API that is used in the 30 | examples. Open the URL 31 | http://localhost:2808/static/browser/example 33 | in your browser. If the URL in your browser's address bar says 34 | file:... the examples on this page will not work. 35 |

36 |
37 | 38 |

Configuration

39 |
40 |
41 | 44 |
45 |
46 | 47 |

Setup

48 |
49 | 50 |
51 | 52 |
53 | 54 |

Plain Vanilla Link Following

55 |
56 |
57 | 58 |
59 |
60 | Start Request 61 |
62 |

 63 |       
64 |
65 | 66 |

JSONPath

67 |
68 |
69 | 70 |
71 |
72 | Start Request 73 |
74 |

 75 |       
76 |
77 | 78 |

URI Templates

79 |
80 |
81 | 82 |
83 |
84 | Start Request 85 |
86 |

 87 |       
88 | 89 |

Post

90 |
91 |
92 | 93 |
94 |
95 | Start Request 96 |
97 |

 98 |       
99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /browser/example/traverson-angular-example.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | var app = 7 | angular.module('traverson-angular-example', 8 | ['traverson', 'ngSanitize']); 9 | 10 | var rootUri = '/'; 11 | 12 | app.service('apiService', function(traverson) { 13 | 14 | var api = traverson.from(rootUri).json(); 15 | 16 | this.setUseAngularHttp = function(val) { 17 | if (val) { 18 | api.useAngularHttp(); 19 | } else { 20 | api = traverson.from(rootUri).json(); 21 | } 22 | }; 23 | 24 | this.plainVanilla = function() { 25 | return api 26 | .follow('second', 'doc') 27 | .getResource(); 28 | }; 29 | 30 | this.jsonPath = function() { 31 | return api 32 | .follow('$.jsonpath.nested.key') 33 | .getResource(); 34 | }; 35 | 36 | this.uriTemplate = function() { 37 | return api 38 | .follow('uri_template') 39 | .withTemplateParameters({ param: 'foobar', id: 13 }) 40 | .getResource(); 41 | }; 42 | 43 | this.post = function() { 44 | return api 45 | .follow('post_link') 46 | .post({ payload: 'this is the payload' }); 47 | }; 48 | }); 49 | 50 | app.controller('main', function($scope) { 51 | $scope.config = { 52 | use$http: false, 53 | }; 54 | }); 55 | 56 | app.controller('generalSetup', function($scope, apiService) { 57 | function code() { 58 | return 'var rootUri = \'' + rootUri + '\';
' + 59 | 'var api = traverson.from(rootUri).json()' + 60 | ($scope.config.use$http ? '.useAngularHttp()' : '') + 61 | ';
'; 62 | } 63 | $scope.code = code(); 64 | $scope.$parent.$watch('config.use$http', function(newValue) { 65 | console.log(newValue); 66 | apiService.setUseAngularHttp(newValue); 67 | $scope.code = code(); 68 | }); 69 | }); 70 | 71 | app.controller('plainVanillaController', function($scope, apiService) { 72 | 73 | $scope.start = function() { 74 | $scope.response = '... talking to server, please stand by ...'; 75 | apiService.plainVanilla().result.then(function(resource) { 76 | $scope.response = JSON.stringify(resource, null, 2); 77 | }, function(err) { 78 | $scope.response = err.message || JSON.stringify(err); 79 | }); 80 | }; 81 | 82 | $scope.code = 83 | 'api
' + 84 | '.follow(\'second\', \'doc\')
' + 85 | '.getResource()
' + 86 | '.result
' + 87 | '.then(function(resource) {
' + 88 | ' // do something with the resource...
' + 89 | '});'; 90 | }); 91 | 92 | app.controller('jsonPathController', function($scope, apiService) { 93 | $scope.start = function() { 94 | $scope.response = '... talking to server, please stand by ...'; 95 | apiService.jsonPath().result.then(function(resource) { 96 | $scope.response = JSON.stringify(resource, null, 2); 97 | }, function(err) { 98 | $scope.response = err.message || JSON.stringify(err); 99 | }); 100 | }; 101 | 102 | $scope.code = 103 | 'api
' + 104 | '.follow(\'$.jsonpath.nested.key\')
' + 105 | '.getResource()
' + 106 | '.result
' + 107 | '.then(function(resource) {
' + 108 | ' // do something with the resource...
' + 109 | '});'; 110 | }); 111 | 112 | app.controller('uriTemplateController', function($scope, apiService) { 113 | $scope.start = function() { 114 | $scope.response = '... talking to server, please stand by ...'; 115 | apiService.uriTemplate().result.then(function(resource) { 116 | $scope.response = JSON.stringify(resource, null, 2); 117 | }, function(err) { 118 | $scope.response = err.message || JSON.stringify(err); 119 | }); 120 | }; 121 | 122 | $scope.code = 123 | 'api
' + 124 | '.follow(\'uri_template\')
' + 125 | '.withTemplateParameters({param: \'foobar\', id: 13})
' + 126 | '.getResource()
' + 127 | '.result
' + 128 | '.then(function(resource) {
' + 129 | ' // do something with the resource...
' + 130 | '});'; 131 | }); 132 | 133 | app.controller('postController', function($scope, apiService) { 134 | $scope.start = function() { 135 | $scope.response = '... talking to server, please stand by ...'; 136 | apiService.post().result.then(function(response) { 137 | var body = response.body; 138 | var resource = JSON.parse(body); 139 | $scope.response = JSON.stringify(resource, null, 2); 140 | }, function(err) { 141 | $scope.response = err.message || JSON.stringify(err); 142 | }); 143 | }; 144 | 145 | $scope.code = 146 | 'api
' + 147 | '.json()
' + 148 | '.follow(\'post_link\')
' + 149 | '.post({ payload: \'this is the payload\' });
' + 150 | '.result
' + 151 | '.then(function(resource) {
' + 152 | ' // do something with the resource...
' + 153 | '});'; 154 | }); 155 | 156 | })(); 157 | -------------------------------------------------------------------------------- /browser/test/README.md: -------------------------------------------------------------------------------- 1 | This folder contains a test harness to run the mocha tests in the browser. You need to start the test server with `node bin/start-test-server.js`, which serves the index.html page as well as the REST API that is used in some of the tests. Then open the URL `http://localhost:2808/static/browser/test` and watch the mocha tests run. Alternatively, just start `grunt`, which starts the test server, runs the tests via PhantomJS and stops the test server again. 2 | -------------------------------------------------------------------------------- /browser/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Browser Tests For traverson-angular 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /greenkeeper.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": { 3 | "default": { 4 | "packages": [ 5 | "package.json" 6 | ] 7 | } 8 | }, 9 | "ignore": ["load-grunt-tasks"] 10 | } 11 | -------------------------------------------------------------------------------- /howto_release.markdown: -------------------------------------------------------------------------------- 1 | # Checklist For Publishing a New Release 2 | 3 | To release version x.y.z: 4 | 5 | - Update release notes 6 | - bump version of traverson in package.json 7 | - npm i 8 | - bump version in package.json to x.y.z (should match the used traverson version, at least the major and minor number) 9 | - bump version in package-lock.json to x.y.z (should match the used traverson version, at least the major and minor number) 10 | - bump version in bower.json to x.y.z 11 | - `npm run build` (to create a fresh browser build, also make sure all tests pass etc.) 12 | - First release? Then `bower register package-name git://github.com/user/repo.git` 13 | - `git commit -am"release x.y.z" && git push` 14 | - `npm publish` 15 | - `git checkout -b release-x.y.z` (to create the release branch, required for bower) 16 | - `git add -f browser/dist/traverson-angular.*` (to add the build artifacts to the release branch) 17 | - `git commit -m"add build artifacts for release"` 18 | - `git push origin release-x.y.z` 19 | - [create a new release on github](https://github.com/traverson/traverson-angular/releases/new) 20 | - Tag version: `x.y.z` (without any prefix or suffix) 21 | - Target: The release branch that hast just been created 22 | - Release title === Tag version 23 | - empty description 24 | - add all four JS files from browser/dist as "binaries" to the release 25 | - Publish release 26 | - Why not just create a tag from the branch via git? Because we want to add the build artifacts to the GitHub relase as attachments (for users neither using npm or bower). This is only possible if the release was created via GitHub's web interface. Normal git tags show up as releases there too, but you can't add attachments or edit the release afterwards. Releases created via the web interface create a git tag automatically, however. 27 | - `git checkout master` 28 | - `git branch -D release-x.y.z` 29 | - `git push origin :release-x.y.z` 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "traverson-angular", 3 | "version": "6.1.0", 4 | "description": "AngularJS adapter for Traverson", 5 | "repository": "https://github.com/traverson/traverson-angular.git", 6 | "main": "traverson-angular.js", 7 | "author": { 8 | "name": "Bastian Krol", 9 | "email": "bastian.krol@web.de" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Michael Heuberger", 14 | "email": "michael.heuberger@binarykitchen.com" 15 | }, 16 | { 17 | "name": "Jamie Gaines", 18 | "email": "jamie.gaines@thinkzig.com" 19 | } 20 | ], 21 | "license": "MIT", 22 | "scripts": { 23 | "test": "grunt", 24 | "build": "grunt" 25 | }, 26 | "config": { 27 | "commitizen": { 28 | "path": "./node_modules/cz-conventional-changelog" 29 | } 30 | }, 31 | "keywords": [ 32 | "JSON", 33 | "REST", 34 | "API", 35 | "HATEOAS", 36 | "hypertext", 37 | "hypermedia", 38 | "HAL", 39 | "AngularJS" 40 | ], 41 | "dependencies": { 42 | "traverson": "^6.1.1" 43 | }, 44 | "devDependencies": { 45 | "angular": "^1.6.6", 46 | "angular-mocks": "^1.6.6", 47 | "angular-sanitize": "^1.6.6", 48 | "brfs": "^2.0.0", 49 | "chai": "^4.1.2", 50 | "commitlint-config-traverson": "^1.0.12", 51 | "cz-conventional-changelog": "^3.1.1", 52 | "express": "^4.16.2", 53 | "grunt": "^1.0.4", 54 | "grunt-browserify": "^5.2.0", 55 | "grunt-cli": "^1.2.0", 56 | "grunt-contrib-clean": "^2.0.0", 57 | "grunt-contrib-jshint": "^2.0.0", 58 | "grunt-contrib-uglify": "^4.0.0", 59 | "grunt-contrib-watch": "^1.0.0", 60 | "grunt-mocha": "^1.2.0", 61 | "husky": "^4.2.4", 62 | "load-grunt-tasks": "^3.5.2", 63 | "mocha": "^5.2.0", 64 | "poll-forever": "^1.0.0", 65 | "request": "^2.87.0", 66 | "sinon": "^9.0.1", 67 | "sinon-chai": "^3.2.0", 68 | "traverson-mock-response": "1.1.0", 69 | "traverson-test-server": "1.12.1" 70 | }, 71 | "browser": { 72 | "angular": false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | traverson-angular 2 | ================= 3 | 4 | AngularJS integration for Traverson, the JS Hypermedia Client 5 | ------------------------------------------------------------- 6 | 7 | [![Build Status](https://travis-ci.org/traverson/traverson-angular.png?branch=master)](https://travis-ci.org/traverson/traverson-angular) 8 | [![Dependency Status](https://david-dm.org/traverson/traverson-angular.png)](https://david-dm.org/traverson/traverson-angular) 9 | [![NPM](https://nodei.co/npm/traverson-angular.png?downloads=true&stars=true)](https://nodei.co/npm/traverson-angular/) 10 | [![Greenkeeper badge](https://badges.greenkeeper.io/traverson/traverson-angular.svg)](https://greenkeeper.io/) 11 | 12 | | File Size (browser build) | KB | 13 | |---------------------------|---:| 14 | | minified & gzipped | 19 | 15 | | minified | 66 | 16 | 17 | Introduction 18 | ------------ 19 | 20 | traverson-angular offers seamless integration of [Traverson](https://github.com/traverson/traverson) with AngularJS. Traverson comes in handy when consuming REST APIs that follow the HATEOAS principle, that is, REST APIs that have links between their resources. If you don't know Traverson, you should probably have a look at its [GitHub page](https://github.com/traverson/traverson) or at this [introductory blog post](https://blog.codecentric.de/en/2013/11/traverson/) first. 21 | 22 | traverson-angular wraps Traverson in an AngularJS module and converts the original callback based API into an API based on promises. 23 | 24 | Installation 25 | ------------ 26 | 27 | ### npm 28 | 29 | See [below](#using-npm-and-browserify). 30 | 31 | ### Download 32 | 33 | You can grab a download from the [latest release](https://github.com/traverson/traverson-angular/releases/latest). All downloads include traverson-angular and a bundled Traverson library, so you do not need to include Traverson separately. Here are your options: 34 | 35 | * `traverson-angular.min.js`: Minified build with UMD. This build can be used with a script tag or with an AMD loader like RequireJS (untested). It will register the AngularJS module `traverson`, which you can use as a dependency of your module (see below). **If in doubt, use this build.** 36 | * `traverson-angular.js`: Non-minified build with UMD. Same as above, just larger. 37 | * `traverson.external.min.js`: Minified require/external build. Created with browserify's `--require` parameter and intended to be used (required) from other browserified modules, which were created with `--external traverson-angular`. This build could be used if you use browserify but do not want to bundle traverson-angular and Traverson with your own browserify build but keep it as a separate file. 38 | * `traverson.external.js`: Non-minified require/external build, same as before, just larger. 39 | 40 | ### Bower 41 | 42 | `bower install traverson-angular --save` 43 | 44 | Usage 45 | ----- 46 | 47 | ```javascript 48 | angular.module('my-app', ['traverson']); 49 | ``` 50 | 51 | ```javascript 52 | angular.module('my-app').service('apiService', function(traverson) { 53 | ... 54 | }); 55 | ``` 56 | 57 | Have a look at the examples in the repository: 58 | 59 | * [Example 1](https://github.com/traverson/traverson-angular/blob/master/browser/example/index.html) ([JavaScript here](https://github.com/traverson/traverson-angular/blob/master/browser/example/traverson-angular-example.js)) 60 | * [GitHub API example](https://github.com/traverson/traverson-angular/blob/master/browser/example/github.html) ([JavaScript here](https://github.com/traverson/traverson-angular/blob/master/browser/example/github-example.js)) 61 | 62 | Using npm and Browserify 63 | ------------------------ 64 | 65 | If you are using npm and [Browserify](http://browserify.org/) and writing your [AngularJS app as CommonJS modules](https://blog.codecentric.de/en/2014/08/angularjs-browserify/), instead of downloading a release, you can install it with `npm install traverson-angular -S`. 66 | 67 | This is how your code using traverson-angular would look like: 68 | ```javascript 69 | var angular = require('angular'); 70 | var traverson = require('traverson-angular'); 71 | var app = angular.module('my-app', [traverson.name]); 72 | 73 | ... 74 | 75 | app.service('apiService', function(traverson) { 76 | ... 77 | }); 78 | 79 | ``` 80 | 81 | See [here](https://github.com/traverson/traverson-angular/tree/master/browser/example/browserify) for a complete, working example of a CommonJS based AngularJS app using traverson-angular, build with Browserify. 82 | 83 | To `require` angular-core like this, you need a shim in your package.json, like this: 84 | 85 | ```javascript 86 | { 87 | ... 88 | "dependencies": { 89 | "angular": "^1.3.4", 90 | ... 91 | }, 92 | "browser": { 93 | "angular": "./angular/angular-common-js.js" 94 | } 95 | } 96 | 97 | ``` 98 | 99 | `angular-common-js.js:` 100 | ```javascript 101 | require('./angular.js'); 102 | module.exports = angular; 103 | ``` 104 | 105 | Browserify your app as usual - Browserify will include traverson-angular, Traverson itself and its dependencies for you. 106 | 107 | API 108 | --- 109 | 110 | You should refer to [Traverson's docs](https://github.com/traverson/traverson/blob/master/readme.markdown) for general info how to work with Traverson. Anything that works with Taverson also works with traverson-angular. The only difference is that traverson-angular's methods are not callback-based but work with promises. 111 | 112 | So this code, which uses Traverson directly: 113 |
114 | traverson
115 | .from('http://api.example.com')
116 | .newRequest()
117 | .follow('link_to', 'resource')
118 | .getResource(function(error, document) {
119 |   if (error) {
120 |     console.error('No luck :-)')
121 |   } else {
122 |     console.log('We have followed the path and reached our destination.')
123 |     console.log(JSON.stringify(document))
124 |   }
125 | });
126 | 
127 | becomes this with traverson-angular: 128 |
129 | traverson
130 | .from('http://api.example.com')
131 | .newRequest()
132 | .follow('link_to', 'resource')
133 | .getResource()
134 | .result
135 | .then(function(document) {
136 |   console.log('We have followed the path and reached our destination.')
137 |   console.log(JSON.stringify(document))
138 | }, function(err) {
139 |   console.error('No luck');
140 | });
141 | 
142 | 143 | The only difference is `.getResource(function(error, document) {` => `.getResource().result.then(function(document) {`. 144 | 145 | Actually, the object returned by `getResource` has three properties: 146 | * `result`: the promise representing the link traversal, 147 | * `continue`: a function that can be used to [continue](#continuing-a-link-traversal) a finished link traversal and 148 | * `abort`: a function that can be used to [abort](#aborting-the-link-traversal) link traversal that is in progress. 149 | 150 | The following action methods of the Traverson request builder return such an object (`{ result, continue, abort }`) when used via traverson-angular: 151 | 152 | * `get()` 153 | * `getResource()` 154 | * `getUri()` 155 | * `post(payload)` 156 | * `put(payload)` 157 | * `patch(payload)` 158 | * `delete` 159 | 160 | ### How HTTP Status Code Are Handled 161 | 162 | In contrast to AngularJS' `$http` service, Traverson and traverson-angular do not interpret status codes outside of the 2xx range as an error condition. Only network problems (host not reachable, timeouts, etc.) lead to a rejection of the promise, that is, only those trigger the error callback. Completed HTTP requests, even those with status 4xx or 5xx are interpreted as a success and trigger the success callback. This applies only to the last request in a traversal, HTTP requests *during* the traversal that respond with 4xx/5xx are interpreted as an error (because the traversal can not continue). 163 | 164 | This also holds when using `.useAngularHttp()` (see below). 165 | 166 | ### Using AngularJS' $http Service Instead Of Traverson's HTTP Module 167 | 168 | Traverson has it's own HTTP module (based on [superagent](https://github.com/visionmedia/superagent)) and by default, this is used to make HTTP requests. If you want to use Traverson in a project that makes use of AngularJS' $http service and its configuration possibilities (default headers, interceptors and so on), these configurations do not apply automatically to the requests issued by Traverson. If you want that, you can configure traverson-angular to use $http instead of Traverson's HTTP module by calling `useAngularHttp()` on the request builder. 169 | 170 | Example: 171 | 172 |
173 | traverson
174 | .from('http://api.example.com')
175 | .useAngularHttp()
176 | .newRequest()
177 | .follow('link_to', 'resource')
178 | .getResource()
179 | .result
180 | .then(function(document) {
181 |   ...
182 | });
183 | 
184 | 185 | ### Continuing a Link Traversal 186 | 187 | See [Traverson's README](https://github.com/traverson/traverson#continuing-a-link-traversal) for a general description of the `continue()` feature. This section just describes how to use it with traverson-angular. 188 | 189 | The object returned by the action methods (`get`, `getResource`, `getUrl`, `post`, `put`, `patch`, `delete`) have a property `continue` which is a function that can be used to obtain a promise that is resolved when the link traversal finishes (as does the `result` promise) and which gives you a request builder instance that starts at the last URL/resource of the finished link traversal. It can be used just as the standard [request builder](https://github.com/traverson/traverson/blob/master/api.markdown#request-builder). That is, it has the same configuration and action methods. It enables you to continue the link traversal from the last target resource and follow more links from there. 190 | 191 | So while with plain vanilla Traverson (not traverson-angular) you would continue a successful link traversal process like this: 192 | 193 | ```javascript 194 | traverson 195 | .from(rootUrl) 196 | .follow('link1', 'link2') 197 | .getResource(function(err, firstResource, traversal) { 198 | if (err) { return done(err); } 199 | // do something with the first resource, maybe decide where to go from here. 200 | traversal 201 | .continue() 202 | .follow('link3', 'link3') 203 | .getResource(function(err, secondResource) { 204 | if (err) { return done(err); } 205 | // do something with the second resource 206 | }); 207 | }); 208 | ``` 209 | 210 | ...this is how it is done with traverson-angular: 211 | 212 |
213 | var request =
214 | traverson
215 | .from('http://api.example.com')
216 | .follow('link1', 'link2');
217 | .getResource();
218 | 
219 | request.result.then(successCallback, errorCallback);
220 | 
221 | request.continue().then(function(request) {
222 |   request
223 |   .follow('link3', 'link4');
224 |   .getResource()
225 |   .result
226 |   .then(successCallback2, errorCallback2);
227 | });
228 | 
229 | 230 | ### Aborting the Link Traversal 231 | 232 | As mentioned above, the object returned by the action methods returns an object which also has an `abort()` function. 233 | 234 | So while with plain vanilla Traverson (not traverson-angular) you would abort a link traversal process like this 235 | 236 |
237 | var handle =
238 | traverson
239 | .from('http://api.example.com')
240 | .newRequest()
241 | .follow('link_to', 'resource')
242 | .getResource(...);
243 | 
244 | // abort the link traversal
245 | handle.abort();
246 | 
247 | 248 | ...this is how it is done with traverson-angular: 249 | 250 |
251 | var handle =
252 | traverson
253 | .from('http://api.example.com')
254 | .newRequest()
255 | .follow('link_to', 'resource')
256 | .getResource();
257 | 
258 | // register callbacks
259 | handle.result.then(successCallback, errorCallback);
260 | 
261 | // abort the link traversal
262 | handle.abort()
263 | 
264 | 265 | traverson-angular With Media Type Plug-Ins 266 | ------------------------------------------ 267 | 268 | You can use all media type plug-ins that are available for Traverson with traverson-angular. Here is how: 269 | 270 | * Make sure the JavaScript for the media type plug-in has been loaded (for example, add a script tag for traverson-hal.min.js). 271 | * Register the media type with a line like this: `traverson.registerMediaType(TraversonJsonHalAdapter.mediaType, TraversonJsonHalAdapter);`. 272 | * If necessary force Traverson to use the media type in question with `setMediaType(...)` (for HAL, you can use the convenience method `.jsonHal()` instead). 273 | * If necessary, add Accept headers so your server knows you want to receive a particular media type. Example: `.withRequestOptions({ headers: { 'accept': 'application/hal+json' } })`. 274 | 275 | Here is a snippet outlining how to use traverson-angular with [traverson-hal](https://github.com/traverson/traverson-hal): 276 | 277 | ```html 278 | 279 | 280 | ``` 281 | 282 | ```javascript 283 | traverson.registerMediaType(TraversonJsonHalAdapter.mediaType, 284 | TraversonJsonHalAdapter); 285 | traverson 286 | .from(rootUri) 287 | .jsonHal() 288 | .withRequestOptions({ headers: { 'accept': 'application/hal+json' } }) 289 | .follow(...) 290 | .getResource() 291 | .result 292 | .then(...); 293 | ``` 294 | 295 | You can find a complete working example for integrating traverson-hal with traverson-anglar in [browser/example/hal.html](https://github.com/traverson/traverson-angular/blob/master/browser/example/hal.html) and [browser/example/hal.js](https://github.com/traverson/traverson-angular/blob/master/browser/example/hal.js). 296 | 297 | 298 | Contributing 299 | ------------ 300 | 301 | See [Contributing to traverson-angular](https://github.com/traverson/traverson-angular/blob/master/CONTRIBUTING.md). 302 | 303 | 304 | Code of Conduct 305 | --------------- 306 | 307 | See [Code of Conduct](https://github.com/traverson/traverson-angular/blob/master/CODE_OF_CONDUCT.md). 308 | 309 | 310 | Release Notes 311 | ------------- 312 | 313 | See [CHANGELOG](https://github.com/traverson/traverson-angular/blob/master/CHANGELOG.md). 314 | 315 | 316 | License 317 | ------- 318 | 319 | MIT 320 | -------------------------------------------------------------------------------- /test/abort_request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai') 4 | , sinon = require('sinon') 5 | , sinonChai = require('sinon-chai') 6 | , assert = chai.assert 7 | , expect = chai.expect 8 | , mockResponse = require('traverson-mock-response')() 9 | , waitFor = require('poll-forever') 10 | , events = require('events') 11 | , EventEmitter = events.EventEmitter 12 | , testUtil = require('./util') 13 | , traversonAngular = require('./angular_test_helper'); 14 | 15 | chai.use(sinonChai); 16 | 17 | function RequestMock() { 18 | EventEmitter.call(this); 19 | } 20 | testUtil.inherits(RequestMock, EventEmitter); 21 | 22 | RequestMock.prototype.abort = function() { 23 | this.emit('abort'); 24 | }; 25 | 26 | describe('Aborting', function() { 27 | 28 | var rootUri = 'http://api.example.org'; 29 | var api = traversonAngular.from(rootUri).json(); 30 | var get; 31 | var successCallback; 32 | var errorCallback; 33 | 34 | beforeEach(function() { 35 | get = sinon.stub(); 36 | api.requestModuleInstance = { get: get }; 37 | successCallback = sinon.spy(); 38 | errorCallback = sinon.spy(); 39 | }); 40 | 41 | it('should abort the link traversal process', function(done) { 42 | var path1 = rootUri + '/path/1'; 43 | var path2 = rootUri + '/path/2'; 44 | 45 | var root = mockResponse({ link1: path1 }); 46 | var response2 = mockResponse({ link2: path2 }); 47 | var result = mockResponse({ should: 'not reach this' }); 48 | 49 | get.returns(new RequestMock()); 50 | 51 | get 52 | .withArgs(rootUri, sinon.match.any, sinon.match.func) 53 | .callsArgWithAsync(2, null, root); 54 | var secondGet = get 55 | .withArgs(path1, sinon.match.any, sinon.match.func); 56 | secondGet 57 | .callsArgWithAsync(2, null, response2); 58 | 59 | var handle = api 60 | .newRequest() 61 | .follow('link1', 'link2', 'link3') 62 | .getResource(); 63 | 64 | handle.result.then(successCallback, errorCallback); 65 | 66 | waitFor( 67 | function() { 68 | return secondGet.called; 69 | }, 70 | function() { 71 | handle.abort(); 72 | waitFor( 73 | function() { 74 | return errorCallback.called; 75 | }, 76 | function() { 77 | assert(errorCallback.calledOnce); 78 | expect(successCallback).to.not.have.been.called; 79 | expect(errorCallback).to.have.been.calledWith(sinon.match. 80 | instanceOf(Error)); 81 | var error = errorCallback.args[0][0]; 82 | expect(error.message) 83 | .to.equal('Link traversal process has been aborted.'); 84 | done(); 85 | } 86 | ); 87 | } 88 | ); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/angular_test_helper.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 'use strict'; 3 | 4 | var testModule = angular.module('testModule', ['traverson']) 5 | , injector = angular.injector(['ng', 'testModule']) 6 | , traversonAngular = injector.get('traverson') 7 | ; 8 | 9 | module.exports = traversonAngular; 10 | -------------------------------------------------------------------------------- /test/browser_suite.js: -------------------------------------------------------------------------------- 1 | require('./abort_request.js'); 2 | require('./continue.js'); 3 | require('./json_get_resource'); 4 | require('./json_requests.js'); 5 | require('./localhost.js'); 6 | require('./using_angular_mocks.js'); 7 | -------------------------------------------------------------------------------- /test/continue.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | /* jshint maxparams: 6 */ 3 | /* jshint maxcomplexity: 12 */ 4 | 5 | 'use strict'; 6 | 7 | var util = require('util') 8 | , mockResponse = require('traverson-mock-response')() 9 | , waitFor = require('poll-forever') 10 | , chai = require('chai') 11 | , sinon = require('sinon') 12 | , sinonChai = require('sinon-chai') 13 | , assert = chai.assert 14 | , expect = chai.expect 15 | , traversonAngular = require('./angular_test_helper'); 16 | 17 | chai.use(sinonChai); 18 | 19 | // TODOs: 20 | // - error handling in a continued traversal 21 | // - cloning with traversal.continue().newRequest(), splitting into multiple 22 | // follow up traversals 23 | // - abort a continuation 24 | // - mixed continuations (first with getResource second with get or vice versa 25 | // plus other combinations, getUrl, post, ...) 26 | 27 | describe('Continuation of traversals', function() { 28 | 29 | var get; 30 | var post; 31 | var put; 32 | var patch; 33 | var del; 34 | 35 | var successCallback1; 36 | var successCallback2; 37 | var errorCallback1; 38 | var errorCallback2; 39 | 40 | var rootUrl = 'http://api.example.org'; 41 | var api = traversonAngular.from(rootUrl).json(); 42 | 43 | var url1 = rootUrl + '/1'; 44 | var rootResponse = mockResponse({ link1: url1 }); 45 | var url2 = rootUrl + '/2'; 46 | var response2 = mockResponse({ link2: url2 }); 47 | var url3 = rootUrl + '/3'; 48 | var response3 = mockResponse({ link3: url3 }); 49 | var response4 = mockResponse({ foo: 'bar' }); 50 | 51 | var payload = { some: 'stuff' }; 52 | 53 | beforeEach(function() { 54 | get = sinon.stub(); 55 | post = sinon.stub(); 56 | put = sinon.stub(); 57 | patch = sinon.stub(); 58 | del = sinon.stub(); 59 | api.requestModuleInstance = { 60 | get: get, 61 | post: post, 62 | put: put, 63 | patch: patch, 64 | del: del, 65 | }; 66 | successCallback1 = sinon.spy(); 67 | successCallback2 = sinon.spy(); 68 | errorCallback1 = sinon.spy(); 69 | errorCallback2 = sinon.spy(); 70 | setupMocks(); 71 | }); 72 | 73 | describe('get', function() { 74 | defineTestsForMethod(api.get); 75 | }); 76 | 77 | describe('getResource', function() { 78 | defineTestsForMethod(api.getResource); 79 | }); 80 | 81 | describe('getUrl', function() { 82 | defineTestsForMethod(api.getUrl); 83 | }); 84 | 85 | describe('post', function() { 86 | defineTestsForMethod(api.post, payload); 87 | }); 88 | 89 | describe('put', function() { 90 | defineTestsForMethod(api.put, payload); 91 | }); 92 | 93 | describe('patch', function() { 94 | defineTestsForMethod(api.patch, payload); 95 | }); 96 | 97 | describe('delete', function() { 98 | defineTestsForMethod(api.delete); 99 | }); 100 | 101 | it('should branch out with continue and newRequest', function(done) { 102 | var successCallback3 = sinon.spy(); 103 | var errorCallback3 = sinon.spy(); 104 | var successCallback4 = sinon.spy(); 105 | var errorCallback4 = sinon.spy(); 106 | var request = api 107 | .newRequest() 108 | .follow('link1') 109 | .getResource(); 110 | request.result.then(successCallback1, errorCallback1); 111 | request.continue().then(function(nextBuilder) { 112 | var request1 = nextBuilder.newRequest(); 113 | var request2 = nextBuilder.newRequest(); 114 | request1 115 | .follow('link2') 116 | .getResource() 117 | .result 118 | .then(successCallback3, errorCallback3); 119 | request2 120 | .follow('link2', 'link3') 121 | .getResource() 122 | .result 123 | .then(successCallback4, errorCallback4); 124 | }); 125 | waitFor( 126 | function() { 127 | console.log('successCallback1.called', successCallback1.called); 128 | console.log('successCallback3.called', successCallback3.called); 129 | console.log('successCallback4.called', successCallback4.called); 130 | return successCallback1.called && 131 | successCallback3.called && 132 | successCallback4.called; 133 | }, 134 | function() { 135 | expect(errorCallback1).to.not.have.been.called; 136 | expect(errorCallback3).to.not.have.been.called; 137 | expect(errorCallback4).to.not.have.been.called; 138 | expect(successCallback1).to.have.been.calledWith(response2.doc); 139 | expect(successCallback3).to.have.been.calledWith(response3.doc); 140 | expect(successCallback4).to.have.been.calledWith(response4.doc); 141 | expect(get.callCount).to.equal(5); 142 | done(); 143 | } 144 | ); 145 | }); 146 | 147 | 148 | function defineTestsForMethod(method, body) { 149 | 150 | it('should continue with links after a no-link traversal', 151 | function(done) { 152 | setupTest( 153 | method, 154 | body, 155 | [], 156 | ['link1', 'link2', 'link3'], { 157 | method: method, 158 | firstResponse: rootResponse, 159 | secondResponse: response4, 160 | expectedUrl1: rootUrl, 161 | expectedUrl2: url3, 162 | expectedNumberOfHttpGetRequests: 4, 163 | noLinksForSecondTraversal: false, 164 | }, done); 165 | }); 166 | 167 | it('should continue with a link (1|1)', function(done) { 168 | setupTest( 169 | method, 170 | body, 171 | ['link1'], 172 | ['link2'], { 173 | method: method, 174 | firstResponse: response2, 175 | secondResponse: response3, 176 | expectedUrl1: url1, 177 | expectedUrl2: url2, 178 | expectedNumberOfHttpGetRequests: 3, 179 | noLinksForSecondTraversal: false, 180 | }, done); 181 | }); 182 | 183 | it('should continue with a link (2|1)', function(done) { 184 | setupTest( 185 | method, 186 | body, 187 | ['link1', 'link2'], 188 | ['link3'], { 189 | method: method, 190 | firstResponse: response3, 191 | secondResponse: response4, 192 | expectedUrl1: url2, 193 | expectedUrl2: url3, 194 | expectedNumberOfHttpGetRequests: 4, 195 | noLinksForSecondTraversal: false, 196 | }, done); 197 | }); 198 | 199 | it('should continue with a link (1|2)', function(done) { 200 | setupTest( 201 | method, 202 | body, 203 | ['link1'], 204 | ['link2', 'link3'], { 205 | method: method, 206 | firstResponse: response2, 207 | secondResponse: response4, 208 | expectedUrl1: url1, 209 | expectedUrl2: url3, 210 | expectedNumberOfHttpGetRequests: 4, 211 | noLinksForSecondTraversal: false, 212 | }, done); 213 | }); 214 | 215 | it('should continue with no links', function(done) { 216 | setupTest( 217 | method, 218 | body, 219 | ['link1', 'link2', 'link3'], 220 | [], { 221 | method: method, 222 | firstResponse: response4, 223 | secondResponse: response4, 224 | expectedUrl1: url3, 225 | expectedUrl2: url3, 226 | expectedNumberOfHttpGetRequests: 4, 227 | noLinksForSecondTraversal: true, 228 | }, done); 229 | }); 230 | 231 | it('should continue with no links after a no-link traversal', 232 | function(done) { 233 | setupTest( 234 | method, 235 | body, 236 | [], 237 | [], { 238 | method: method, 239 | firstResponse: rootResponse, 240 | secondResponse: rootResponse, 241 | expectedUrl1: rootUrl, 242 | expectedUrl2: rootUrl, 243 | expectedNumberOfHttpGetRequests: 1, 244 | noLinksForSecondTraversal: true, 245 | }, done); 246 | }); 247 | } // function defineTestsForMethod 248 | 249 | function setupMocks() { 250 | [get, post, put, patch, del].forEach(function(fn) { 251 | fn 252 | .withArgs(rootUrl, sinon.match.object, sinon.match.func) 253 | .callsArgWithAsync(2, null, rootResponse); 254 | fn 255 | .withArgs(url1, sinon.match.object, sinon.match.func) 256 | .callsArgWithAsync(2, null, response2); 257 | fn 258 | .withArgs(url2, sinon.match.object, sinon.match.func) 259 | .callsArgWithAsync(2, null, response3); 260 | fn 261 | .withArgs(url3, sinon.match.object, sinon.match.func) 262 | .callsArgWithAsync(2, null, response4); 263 | }); 264 | } 265 | 266 | function setupTest(method, body, links1, links2, results, done) { 267 | var builder = api.newRequest().follow(links1); 268 | 269 | var request = method.apply(builder, (body ? [body] : [])); 270 | request.result.then(successCallback1, errorCallback1); 271 | 272 | request.continue().then(function(nextBuilder) { 273 | nextBuilder.follow(links2); 274 | method 275 | .apply(nextBuilder, (body ? [body] : [])) 276 | .result 277 | .then(successCallback2, errorCallback2); 278 | }); 279 | 280 | waitFor( 281 | function() { 282 | return successCallback1.called && 283 | successCallback2.called; 284 | }, 285 | function() { 286 | checkResult(results); 287 | done(); 288 | } 289 | ); 290 | } 291 | 292 | function checkResult(results) { 293 | expect(errorCallback1).to.not.have.been.called; 294 | expect(errorCallback2).to.not.have.been.called; 295 | if (results.method === api.get) { 296 | expect(successCallback1).to.have.been.calledWith( 297 | results.firstResponse); 298 | expect(successCallback2).to.have.been.calledWith( 299 | results.secondResponse); 300 | expect(get.callCount).to.equal(results.expectedNumberOfHttpGetRequests); 301 | } else if (results.method === api.getResource) { 302 | expect(successCallback1).to.have.been.calledWith( 303 | results.firstResponse.doc); 304 | expect(successCallback2).to.have.been.calledWith( 305 | results.secondResponse.doc); 306 | expect(get.callCount).to.equal(results.expectedNumberOfHttpGetRequests); 307 | } else if (results.method === api.getUrl) { 308 | expect(successCallback1).to.have.been.calledWith( 309 | results.expectedUrl1); 310 | expect(successCallback2).to.have.been.calledWith( 311 | results.expectedUrl2); 312 | expect(get.callCount).to.equal( 313 | results.expectedNumberOfHttpGetRequests - 1); 314 | } else if (results.method === api.post) { 315 | expect(successCallback1).to.have.been.calledWith( 316 | results.firstResponse); 317 | expect(successCallback2).to.have.been.calledWith( 318 | results.secondResponse); 319 | expect(get.callCount).to.equal(results.expectedNumberOfHttpGetRequests - 320 | (results.noLinksForSecondTraversal ? 1 : 2)); 321 | expect(post.callCount).to.equal(2); 322 | } else if (results.method === api.put) { 323 | expect(successCallback1).to.have.been.calledWith( 324 | results.firstResponse); 325 | expect(successCallback2).to.have.been.calledWith( 326 | results.secondResponse); 327 | expect(get.callCount).to.equal(results.expectedNumberOfHttpGetRequests - 328 | (results.noLinksForSecondTraversal ? 1 : 2)); 329 | expect(put.callCount).to.equal(2); 330 | } else if (results.method === api.patch) { 331 | expect(successCallback1).to.have.been.calledWith( 332 | results.firstResponse); 333 | expect(successCallback2).to.have.been.calledWith( 334 | results.secondResponse); 335 | expect(get.callCount).to.equal(results.expectedNumberOfHttpGetRequests - 336 | (results.noLinksForSecondTraversal ? 1 : 2)); 337 | expect(patch.callCount).to.equal(2); 338 | } else if (results.method === api.delete) { 339 | expect(successCallback1).to.have.been.calledWith( 340 | results.firstResponse); 341 | expect(successCallback2).to.have.been.calledWith( 342 | results.secondResponse); 343 | expect(get.callCount).to.equal(results.expectedNumberOfHttpGetRequests - 344 | (results.noLinksForSecondTraversal ? 1 : 2)); 345 | expect(del.callCount).to.equal(2); 346 | } else { 347 | throw new Error('Unknown method: ' + results.method.name + ': ' + 348 | results.method); 349 | } 350 | } 351 | 352 | }); 353 | -------------------------------------------------------------------------------- /test/json_get_resource.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 'use strict'; 3 | 4 | var chai = require('chai') 5 | , sinon = require('sinon') 6 | , sinonChai = require('sinon-chai') 7 | , mockResponse = require('traverson-mock-response')() 8 | , waitFor = require('poll-forever') 9 | , assert = chai.assert 10 | , expect = chai.expect 11 | , traversonAngular = require('./angular_test_helper'); 12 | 13 | chai.use(sinonChai); 14 | 15 | describe('traverson-angular', function() { 16 | 17 | var rootUri = 'http://api.example.org'; 18 | var api = traversonAngular.from(rootUri).json(); 19 | var get; 20 | var successCallback; 21 | var errorCallback; 22 | 23 | var result = mockResponse({ foo: 'bar' }); 24 | 25 | beforeEach(function() { 26 | get = sinon.stub(); 27 | api.requestModuleInstance = { get: get }; 28 | successCallback = sinon.spy(); 29 | errorCallback = sinon.spy(); 30 | }); 31 | 32 | describe('using Traverson\'s basic features', function() { 33 | var rootStep = { 34 | uri: rootUri 35 | }; 36 | var rootResponse = mockResponse({ 37 | irrelevant: { stuff: 'to be ignored' }, 38 | link: rootUri + '/link/to/thing', 39 | more: { stuff: { that: 'we do not care about' } } 40 | }); 41 | 42 | it('should access the root URI', function() { 43 | api 44 | .newRequest() 45 | .follow() 46 | .getResource() 47 | .result 48 | .then(successCallback, errorCallback); 49 | expect(get).to.have.been.calledWith( 50 | rootUri, sinon.match.any, sinon.match.func); 51 | }); 52 | 53 | it('should call successCallback with the root doc', function(done) { 54 | get.callsArgWithAsync(2, null, rootResponse); 55 | 56 | api 57 | .newRequest() 58 | .follow() 59 | .getResource() 60 | .result 61 | .then(successCallback, errorCallback); 62 | 63 | waitFor( 64 | function() { return successCallback.called; }, 65 | function() { 66 | expect(successCallback).to.have.been.calledWith(rootResponse.doc); 67 | expect(errorCallback).to.not.have.been.called; 68 | done(); 69 | } 70 | ); 71 | }); 72 | 73 | it('should call errorCallback with err', function(done) { 74 | var err = new Error('test error'); 75 | get.callsArgWithAsync(2, err); 76 | 77 | api 78 | .follow() 79 | .getResource() 80 | .result 81 | .then(successCallback, errorCallback); 82 | 83 | waitFor( 84 | function() { return errorCallback.called; }, 85 | function() { 86 | expect(successCallback).to.not.have.been.called; 87 | expect(errorCallback).to.have.been.calledWith(err); 88 | done(); 89 | } 90 | ); 91 | }); 92 | 93 | it('should follow a single element path', function(done) { 94 | get 95 | .withArgs(rootUri, sinon.match.any, sinon.match.func) 96 | .callsArgWithAsync(2, null, rootResponse); 97 | get 98 | .withArgs(rootUri + '/link/to/thing', sinon.match.any, sinon.match.func) 99 | .callsArgWithAsync(2, null, result); 100 | 101 | api 102 | .newRequest() 103 | .follow('link') 104 | .getResource() 105 | .result 106 | .then(successCallback, errorCallback); 107 | 108 | waitFor( 109 | function() { return successCallback.called; }, 110 | function() { 111 | expect(successCallback).to.have.been.calledWith(result.doc); 112 | expect(errorCallback).to.not.have.been.called; 113 | done(); 114 | } 115 | ); 116 | }); 117 | 118 | it('should follow a single element path as array', function(done) { 119 | get 120 | .withArgs(rootUri, sinon.match.any, sinon.match.func) 121 | .callsArgWithAsync(2, null, rootResponse); 122 | get 123 | .withArgs(rootUri + '/link/to/thing', sinon.match.any, sinon.match.func) 124 | .callsArgWithAsync(2, null, result); 125 | 126 | api 127 | .newRequest() 128 | .follow(['link']) 129 | .getResource() 130 | .result 131 | .then(successCallback, errorCallback); 132 | 133 | waitFor( 134 | function() { return successCallback.called; }, 135 | function() { 136 | expect(successCallback).to.have.been.calledWith(result.doc); 137 | expect(errorCallback).to.not.have.been.called; 138 | done(); 139 | } 140 | ); 141 | }); 142 | 143 | it('should call errorCallback with err if link is not found', 144 | function(done) { 145 | get 146 | .withArgs(rootUri, sinon.match.any, sinon.match.func) 147 | .callsArgWithAsync(2, null, rootResponse); 148 | 149 | api 150 | .newRequest() 151 | .follow('non-existing-link') 152 | .getResource() 153 | .result 154 | .then(successCallback, errorCallback); 155 | 156 | waitFor( 157 | function() { return errorCallback.called; }, 158 | function() { 159 | expect(successCallback).to.not.have.been.called; 160 | assert(errorCallback.calledOnce); 161 | expect(errorCallback).to.have.been.calledWith(sinon.match. 162 | instanceOf(Error)); 163 | expect(errorCallback.args[0][0].message).to.contain('Could not ' + 164 | 'find property non-existing-link'); 165 | done(); 166 | } 167 | ); 168 | }); 169 | 170 | it('should call errorCallback with err inside recursion', function(done) { 171 | var err = new Error('test error'); 172 | 173 | get 174 | .withArgs(rootUri, sinon.match.any, sinon.match.func) 175 | .callsArgWithAsync(2, null, 176 | mockResponse({ firstLink: rootUri + '/first' })); 177 | get 178 | .withArgs(rootUri + '/first', sinon.match.any, sinon.match.func) 179 | .callsArgWithAsync(2, err); 180 | 181 | api 182 | .newRequest() 183 | .follow('firstLink') 184 | .getResource() 185 | .result 186 | .then(successCallback, errorCallback); 187 | 188 | waitFor( 189 | function() { return errorCallback.called; }, 190 | function() { 191 | expect(successCallback).to.not.have.been.called; 192 | expect(errorCallback).to.have.been.calledWith(err); 193 | done(); 194 | } 195 | ); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/json_requests.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 'use strict'; 3 | 4 | var chai = require('chai') 5 | , sinon = require('sinon') 6 | , sinonChai = require('sinon-chai') 7 | , mockResponse = require('traverson-mock-response')() 8 | , waitFor = require('poll-forever') 9 | , assert = chai.assert 10 | , expect = chai.expect 11 | , traversonAngular = require('./angular_test_helper'); 12 | 13 | chai.use(sinonChai); 14 | 15 | /* 16 | * Tests for all of Json Walker's request methods except getResource, which is 17 | * tested extensively in json_get_resource.js. This test suite contains tests 18 | * for get, post, put, delete and patch. Each http method verb has it's own 19 | * describe-section. Since most of the code path is the same for getResource 20 | * and get, post, ..., there are just a few basic tests here for each verb. 21 | * The getResource tests are more comprehensive. 22 | */ 23 | describe('The JSON client\'s', function() { 24 | 25 | var get; 26 | var post; 27 | var put; 28 | var patch; 29 | var del; 30 | 31 | var successCallback; 32 | var errorCallback; 33 | 34 | var rootUri = 'http://api.io'; 35 | var api = traversonAngular.from(rootUri).json(); 36 | 37 | var getUri = rootUri + '/link/to/resource'; 38 | var postUri = rootUri + '/post/something/here'; 39 | var putUri = rootUri + '/put/something/here'; 40 | var patchUri = rootUri + '/patch/me'; 41 | var deleteUri = rootUri + '/delete/me'; 42 | var templateUri = rootUri + '/template/{param}'; 43 | 44 | var rootResponse = mockResponse({ 45 | 'get_link': getUri, 46 | 'post_link': postUri, 47 | 'put_link': putUri, 48 | 'patch_link': patchUri, 49 | 'delete_link': deleteUri, 50 | 'template_link': templateUri 51 | }); 52 | 53 | var result = mockResponse({ result: 'success' }); 54 | 55 | var payload = { 56 | some: 'stuff', 57 | data: 4711 58 | }; 59 | 60 | beforeEach(function() { 61 | get = sinon.stub(); 62 | post = sinon.stub(); 63 | put = sinon.stub(); 64 | patch = sinon.stub(); 65 | del = sinon.stub(); 66 | api.requestModuleInstance = { 67 | get: get, 68 | post: post, 69 | put: put, 70 | patch: patch, 71 | del: del, 72 | }; 73 | successCallback = sinon.spy(); 74 | errorCallback = sinon.spy(); 75 | 76 | get 77 | .withArgs(rootUri, sinon.match.any, sinon.match.func) 78 | .callsArgWithAsync(2, null, rootResponse, rootResponse.body); 79 | get 80 | .withArgs(getUri, sinon.match.any, sinon.match.func) 81 | .callsArgWithAsync(2, null, result, result.body); 82 | get 83 | .withArgs(postUri, sinon.match.any, sinon.match.func) 84 | .callsArgWithAsync(2, 85 | new Error('GET is not implemented for this URL, please POST ' + 86 | 'something')); 87 | }); 88 | 89 | describe('get method', function() { 90 | 91 | it('should follow the links', function(done) { 92 | api 93 | .newRequest() 94 | .follow('get_link') 95 | .get() 96 | .result 97 | .then(successCallback, errorCallback); 98 | 99 | waitFor( 100 | function() { return successCallback.called; }, 101 | function() { 102 | expect(successCallback).to.have.been.calledWith(result); 103 | expect(errorCallback).to.not.have.been.called; 104 | done(); 105 | } 106 | ); 107 | }); 108 | 109 | it('should call errorCallback with err', function(done) { 110 | var err = new Error('test error'); 111 | // Default stubbing from beforeEach is not what we want here. 112 | // IMO, get.reset() should be enough, but isnt? 113 | get = sinon.stub(); 114 | api.requestModuleInstance = { get: get }; 115 | get.callsArgWithAsync(2, err); 116 | 117 | api 118 | .newRequest() 119 | .get() 120 | .result 121 | .then(successCallback, errorCallback); 122 | 123 | waitFor( 124 | function() { return errorCallback.called; }, 125 | function() { 126 | expect(successCallback).to.not.have.been.called; 127 | expect(errorCallback).to.have.been.calledWith(err); 128 | done(); 129 | } 130 | ); 131 | }); 132 | 133 | it('should call errorCallback with err when walking along the links fails', 134 | function(done) { 135 | var err = new Error('test error'); 136 | // Default stubbing from beforeEach is not what we want here. 137 | // IMO, get.reset() should be enough, but isnt? 138 | get = sinon.stub(); 139 | api.requestModuleInstance = { get: get }; 140 | get 141 | .withArgs(rootUri, sinon.match.any, sinon.match.func) 142 | .callsArgWithAsync(2, null, rootResponse); 143 | get 144 | .withArgs(getUri, sinon.match.any, sinon.match.func) 145 | .callsArgWithAsync(2, err); 146 | 147 | api 148 | .newRequest() 149 | .follow('get_link', 'another_link') 150 | .get() 151 | .result 152 | .then(successCallback, errorCallback); 153 | 154 | waitFor( 155 | function() { return errorCallback.called; }, 156 | function() { 157 | expect(successCallback).to.not.have.been.called; 158 | expect(errorCallback).to.have.been.calledWith(err); 159 | done(); 160 | } 161 | ); 162 | }); 163 | 164 | }); 165 | 166 | describe('getUrl method', function() { 167 | 168 | it('should follow the links and yield the last URL', function(done) { 169 | api 170 | .newRequest() 171 | .follow('get_link') 172 | .getUrl() 173 | .result 174 | .then(successCallback, errorCallback); 175 | 176 | waitFor( 177 | function() { return successCallback.called; }, 178 | function() { 179 | expect(successCallback).to.have.been.calledWith(getUri); 180 | expect(errorCallback).to.not.have.been.called; 181 | expect(get.callCount).to.equal(1); 182 | done(); 183 | } 184 | ); 185 | }); 186 | 187 | it('should yield resolved URL if last URL is a URI template', 188 | function(done) { 189 | api 190 | .newRequest() 191 | .follow('template_link') 192 | .withTemplateParameters({ param: 'substituted' }) 193 | .getUrl() 194 | .result 195 | .then(successCallback, errorCallback); 196 | 197 | waitFor( 198 | function() { return successCallback.called; }, 199 | function() { 200 | expect(successCallback).to.have.been.calledWith( 201 | rootUri + '/template/substituted'); 202 | expect(errorCallback).to.not.have.been.called; 203 | expect(get.callCount).to.equal(1); 204 | done(); 205 | } 206 | ); 207 | }); 208 | }); 209 | 210 | 211 | describe('post method', function() { 212 | 213 | var result = mockResponse({ result: 'success' }, 201); 214 | 215 | it('should follow the links and post to the last URL', 216 | function(done) { 217 | post 218 | .withArgs(postUri, sinon.match.object, sinon.match.func) 219 | .callsArgWithAsync(2, null, result); 220 | 221 | api 222 | .newRequest() 223 | .follow('post_link') 224 | .post(payload) 225 | .result 226 | .then(successCallback, errorCallback); 227 | 228 | waitFor( 229 | function() { return successCallback.called; }, 230 | function() { 231 | expect(errorCallback).to.not.have.been.called; 232 | expect(successCallback).to.have.been.calledWith(result); 233 | expect(post.firstCall.args[1].body).to.exist; 234 | expect(post.firstCall.args[1].body).to.contain(payload.some); 235 | expect(post.firstCall.args[1].body).to.contain(payload.data); 236 | done(); 237 | } 238 | ); 239 | }); 240 | 241 | it('should call errorCallback with err when post fails', 242 | function(done) { 243 | var err = new Error('test error'); 244 | post 245 | .withArgs(postUri, sinon.match.object, sinon.match.func) 246 | .callsArgWithAsync(2, err); 247 | 248 | api 249 | .newRequest() 250 | .follow('post_link') 251 | .post(payload) 252 | .result 253 | .then(successCallback, errorCallback); 254 | 255 | waitFor( 256 | function() { return errorCallback.called; }, 257 | function() { 258 | expect(errorCallback).to.have.been.calledWith(err); 259 | expect(successCallback).to.not.have.been.called; 260 | done(); 261 | } 262 | ); 263 | }); 264 | 265 | }); 266 | 267 | describe('put method', function() { 268 | 269 | var result = mockResponse({ result: 'success' }, 200); 270 | 271 | it('should follow the links and put to the last URL', 272 | function(done) { 273 | put 274 | .withArgs(putUri, sinon.match.object, sinon.match.func) 275 | .callsArgWithAsync(2, null, result); 276 | 277 | api 278 | .newRequest() 279 | .follow('put_link') 280 | .put(payload) 281 | .result 282 | .then(successCallback, errorCallback); 283 | 284 | waitFor( 285 | function() { return successCallback.called; }, 286 | function() { 287 | expect(successCallback).to.have.been.calledWith(result); 288 | expect(errorCallback).to.not.have.been.called; 289 | expect(put.firstCall.args[1].body).to.exist; 290 | expect(put.firstCall.args[1].body).to.contain(payload.some); 291 | expect(put.firstCall.args[1].body).to.contain(payload.data); 292 | done(); 293 | } 294 | ); 295 | }); 296 | 297 | it('should call errorCallback with err when put fails', 298 | function(done) { 299 | var err = new Error('test error'); 300 | put 301 | .withArgs(putUri, sinon.match.object, sinon.match.func) 302 | .callsArgWithAsync(2, err); 303 | 304 | api 305 | .newRequest() 306 | .follow('put_link') 307 | .put(payload) 308 | .result 309 | .then(successCallback, errorCallback); 310 | 311 | waitFor( 312 | function() { return errorCallback.called; }, 313 | function() { 314 | expect(errorCallback).to.have.been.calledWith(err); 315 | expect(successCallback).to.not.have.been.called; 316 | done(); 317 | } 318 | ); 319 | }); 320 | }); 321 | 322 | describe('patch method', function() { 323 | 324 | var result = mockResponse({ result: 'success' }, 200); 325 | 326 | it('should follow the links and patch the last URL', 327 | function(done) { 328 | patch 329 | .withArgs(patchUri, sinon.match.object, sinon.match.func) 330 | .callsArgWithAsync(2, null, result); 331 | 332 | api 333 | .newRequest() 334 | .follow('patch_link') 335 | .patch(payload) 336 | .result 337 | .then(successCallback, errorCallback); 338 | 339 | waitFor( 340 | function() { return successCallback.called; }, 341 | function() { 342 | expect(successCallback).to.have.been.calledWith(result); 343 | expect(errorCallback).to.not.have.been.called; 344 | expect(patch.firstCall.args[1].body).to.exist; 345 | expect(patch.firstCall.args[1].body).to.contain(payload.some); 346 | expect(patch.firstCall.args[1].body).to.contain(payload.data); 347 | done(); 348 | } 349 | ); 350 | }); 351 | 352 | it('should call errorCallback with err when patch fails', 353 | function(done) { 354 | var err = new Error('test error'); 355 | patch 356 | .withArgs(patchUri, sinon.match.object, sinon.match.func) 357 | .callsArgWithAsync(2, err); 358 | 359 | api 360 | .newRequest() 361 | .follow('patch_link') 362 | .patch(payload) 363 | .result 364 | .then(successCallback, errorCallback); 365 | 366 | waitFor( 367 | function() { return errorCallback.called; }, 368 | function() { 369 | expect(errorCallback).to.have.been.calledWith(err); 370 | expect(successCallback).to.not.have.been.called; 371 | done(); 372 | } 373 | ); 374 | }); 375 | }); 376 | 377 | describe('delete method', function() { 378 | 379 | var result = mockResponse(null, 204); 380 | 381 | it('should follow the links and delete the last URL', 382 | function(done) { 383 | del 384 | .withArgs(deleteUri, sinon.match.object, sinon.match.func) 385 | .callsArgWithAsync(2, null, result); 386 | 387 | api 388 | .newRequest() 389 | .follow('delete_link') 390 | .delete() 391 | .result 392 | .then(successCallback, errorCallback); 393 | 394 | waitFor( 395 | function() { return successCallback.called; }, 396 | function() { 397 | expect(successCallback).to.have.been.calledWith(result); 398 | expect(errorCallback).to.not.have.been.called; 399 | done(); 400 | } 401 | ); 402 | }); 403 | 404 | it('should call errorCallback with err when deleting fails', 405 | function(done) { 406 | var err = new Error('test error'); 407 | del 408 | .withArgs(deleteUri, sinon.match.object, sinon.match.func) 409 | .callsArgWithAsync(2, err); 410 | 411 | api 412 | .newRequest() 413 | .follow('delete_link') 414 | .del() 415 | .result 416 | .then(successCallback, errorCallback); 417 | 418 | waitFor( 419 | function() { return errorCallback.called; }, 420 | function() { 421 | expect(errorCallback).to.have.been.calledWith(err); 422 | expect(successCallback).to.not.have.been.called; 423 | done(); 424 | } 425 | ); 426 | }); 427 | }); 428 | }); 429 | -------------------------------------------------------------------------------- /test/localhost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var waitFor = require('poll-forever') 4 | , chai = require('chai') 5 | , sinon = require('sinon') 6 | , sinonChai = require('sinon-chai') 7 | , traversonAngular = require('./angular_test_helper') 8 | , assert = chai.assert 9 | , expect = chai.expect; 10 | 11 | chai.use(sinonChai); 12 | 13 | describe('traverson-angular (when tested against a local server)', function() { 14 | 15 | var api; 16 | var testServer; 17 | var rootUri = 'http://127.0.0.1:2808/'; 18 | var successCallback; 19 | var errorCallback; 20 | 21 | beforeEach(function() { 22 | api = traversonAngular 23 | .from(rootUri) 24 | .json() 25 | .withRequestOptions({ 26 | headers: { 27 | 'Accept': 'application/json', 28 | 'Content-Type': 'application/json' 29 | } 30 | }); 31 | successCallback = sinon.spy(); 32 | errorCallback = sinon.spy(); 33 | }); 34 | 35 | it('should fetch the root response', function(done) { 36 | api 37 | .newRequest() 38 | .get() 39 | .result 40 | .then(successCallback, errorCallback); 41 | waitFor( 42 | function() { return successCallback.called; }, 43 | function() { 44 | var resultDoc = checkResponseWithBody(); 45 | expect(resultDoc.first).to.exist; 46 | expect(resultDoc.first).to.equal(rootUri + 'first'); 47 | done(); 48 | } 49 | ); 50 | }); 51 | 52 | it('should fetch the root document', function(done) { 53 | api 54 | .newRequest() 55 | .getResource() 56 | .result 57 | .then(successCallback, errorCallback); 58 | waitFor( 59 | function() { return successCallback.called; }, 60 | function() { 61 | var resultDoc = checkResultDoc(); 62 | expect(resultDoc.first).to.exist; 63 | expect(resultDoc.first).to.equal(rootUri + 'first'); 64 | done(); 65 | } 66 | ); 67 | }); 68 | 69 | it('should follow a single element path', function(done) { 70 | api 71 | .newRequest() 72 | .follow('first') 73 | .getResource() 74 | .result 75 | .then(successCallback, errorCallback); 76 | waitFor( 77 | function() { return successCallback.called; }, 78 | function() { 79 | var resultDoc = checkResultDoc(); 80 | expect(resultDoc.first).to.exist; 81 | expect(resultDoc.first).to.equal('document'); 82 | done(); 83 | } 84 | ); 85 | }); 86 | 87 | it('should follow a multi-element path', function(done) { 88 | api 89 | .newRequest() 90 | .follow('second', 'doc') 91 | .get() 92 | .result 93 | .then(successCallback, errorCallback); 94 | waitFor( 95 | function() { return successCallback.called; }, 96 | function() { 97 | var resultDoc = checkResponseWithBody(); 98 | expect(resultDoc.second).to.exist; 99 | expect(resultDoc.second).to.equal('document'); 100 | done(); 101 | } 102 | ); 103 | }); 104 | 105 | it('should follow a multi-element path (/w AngularJS\' $http)', 106 | function(done) { 107 | api 108 | .newRequest() 109 | .useAngularHttp() 110 | .follow('second', 'doc') 111 | .get() 112 | .result 113 | .then(successCallback, errorCallback); 114 | waitFor( 115 | function() { return successCallback.called; }, 116 | function() { 117 | var resultDoc = checkResponseWithBody(); 118 | expect(resultDoc.second).to.exist; 119 | expect(resultDoc.second).to.equal('document'); 120 | done(); 121 | } 122 | ); 123 | }); 124 | 125 | it('should follow a multi-element path to a resource', function(done) { 126 | api 127 | .newRequest() 128 | .follow('second', 'doc') 129 | .getResource() 130 | .result 131 | .then(successCallback, errorCallback); 132 | waitFor( 133 | function() { return successCallback.called; }, 134 | function() { 135 | var resultDoc = checkResultDoc(); 136 | expect(resultDoc.second).to.exist; 137 | expect(resultDoc.second).to.equal('document'); 138 | done(); 139 | } 140 | ); 141 | }); 142 | 143 | it('should follow a multi-element path to a resource (/w AngularJS\' $http)', 144 | function(done) { 145 | api 146 | .newRequest() 147 | .useAngularHttp() 148 | .follow('second', 'doc') 149 | .getResource() 150 | .result 151 | .then(successCallback, errorCallback); 152 | waitFor( 153 | function() { return successCallback.called; }, 154 | function() { 155 | var resultDoc = checkResultDoc(); 156 | expect(resultDoc.second).to.exist; 157 | expect(resultDoc.second).to.equal('document'); 158 | done(); 159 | } 160 | ); 161 | }); 162 | 163 | it('should authenticate', function(done) { 164 | api 165 | .newRequest() 166 | .withRequestOptions({ 167 | auth: { 168 | user: 'traverson', 169 | pass: 'verysecretpassword', 170 | sendImmediately: false 171 | } 172 | }) 173 | .follow('auth') 174 | .getResource() 175 | .result 176 | .then(successCallback, errorCallback); 177 | waitFor( 178 | function() { return successCallback.called; }, 179 | function() { 180 | var resultDoc = checkResultDoc(); 181 | expect(resultDoc.user).to.exist; 182 | expect(resultDoc.user).to.equal('authenticated'); 183 | done(); 184 | } 185 | ); 186 | }); 187 | 188 | it('should authenticate (/w AngularJS\' $http)', function(done) { 189 | api 190 | .newRequest() 191 | .useAngularHttp() 192 | .withRequestOptions({ 193 | auth: { 194 | user: 'traverson', 195 | pass: 'verysecretpassword', 196 | sendImmediately: false 197 | } 198 | }) 199 | .follow('auth') 200 | .getResource() 201 | .result 202 | .then(successCallback, errorCallback); 203 | waitFor( 204 | function() { return successCallback.called; }, 205 | function() { 206 | var resultDoc = checkResultDoc(); 207 | expect(resultDoc.user).to.exist; 208 | expect(resultDoc.user).to.equal('authenticated'); 209 | done(); 210 | } 211 | ); 212 | }); 213 | 214 | it('should leverage JSONPath', function(done) { 215 | api 216 | .newRequest() 217 | .follow('$.jsonpath.nested.key') 218 | .getResource() 219 | .result 220 | .then(successCallback, errorCallback); 221 | waitFor( 222 | function() { return successCallback.called; }, 223 | function() { 224 | var resultDoc = checkResultDoc(); 225 | expect(resultDoc.third).to.exist; 226 | expect(resultDoc.third).to.equal('document'); 227 | done(); 228 | } 229 | ); 230 | }); 231 | 232 | it('should leverage URI templates', function(done) { 233 | api 234 | .newRequest() 235 | .withTemplateParameters({param: 'foobar', id: 13}) 236 | .follow('uri_template') 237 | .getResource() 238 | .result 239 | .then(successCallback, errorCallback); 240 | waitFor( 241 | function() { return successCallback.called; }, 242 | function() { 243 | var resultDoc = checkResultDoc(); 244 | expect(resultDoc.some).to.equal('document'); 245 | expect(resultDoc.param).to.equal('foobar'); 246 | expect(resultDoc.id).to.equal('13'); 247 | done(); 248 | } 249 | ); 250 | }); 251 | 252 | it('should follow the location header', function(done) { 253 | api 254 | .newRequest() 255 | .follow('respond_location') 256 | .followLocationHeader() 257 | .follow('doc') 258 | .getResource() 259 | .result 260 | .then(successCallback, errorCallback); 261 | waitFor( 262 | function() { return successCallback.called; }, 263 | function() { 264 | var resultDoc = checkResultDoc(); 265 | expect(resultDoc).to.eql({ second: 'document' }); 266 | done(); 267 | } 268 | ); 269 | }); 270 | 271 | // this is a 404 *during* the traversal, which is interpreted as an error 272 | // condition 273 | it('should fail gracefully on 404 during traversal (get())', function(done) { 274 | api 275 | .newRequest() 276 | .follow('blind_alley', 'more', 'links') 277 | .get() 278 | .result 279 | .then(successCallback, errorCallback); 280 | waitFor( 281 | function() { return errorCallback.called; }, 282 | function() { 283 | expect(successCallback.callCount).to.equal(0); 284 | expect(errorCallback.callCount).to.equal(1); 285 | var error = errorCallback.firstCall.args[0]; 286 | expect(error).to.exist; 287 | expect(error.name).to.equal('HTTPError'); 288 | expect(error.message).to.equal('HTTP GET request to ' + rootUri + 289 | 'does/not/exist' + ' resulted in HTTP status code 404.'); 290 | expect(error.url).to.equal(rootUri + 'does/not/exist'); 291 | expect(error.httpStatus).to.equal(404); 292 | 293 | var lastBody = error.body; 294 | expect(lastBody).to.exist; 295 | expect(lastBody).to.contain('message'); 296 | expect(lastBody).to.contain('resource not found'); 297 | done(); 298 | } 299 | ); 300 | }); 301 | 302 | // same as above (that is, a 404 *during* the traversal, which is interpreted 303 | // as an error condition), this time using AngularJS' $http service 304 | it('should fail gracefully on 404 during traversal ' + 305 | '(get(), /w AngularJS\' $http)', function(done) { 306 | api 307 | .newRequest() 308 | .useAngularHttp() 309 | .follow('blind_alley', 'more', 'links') 310 | .get() 311 | .result 312 | .then(successCallback, errorCallback); 313 | waitFor( 314 | function() { return errorCallback.called; }, 315 | function() { 316 | expect(successCallback.callCount).to.equal(0); 317 | expect(errorCallback.callCount).to.equal(1); 318 | var error = errorCallback.firstCall.args[0]; 319 | 320 | expect(error).to.exist; 321 | expect(error.name).to.equal('HTTPError'); 322 | expect(error.message).to.equal('HTTP GET request to ' + rootUri + 323 | 'does/not/exist' + ' resulted in HTTP status code 404.'); 324 | expect(error.url).to.equal(rootUri + 'does/not/exist'); 325 | expect(error.httpStatus).to.equal(404); 326 | 327 | var lastBody = error.body; 328 | expect(lastBody).to.exist; 329 | expect(lastBody).to.contain('message'); 330 | expect(lastBody).to.contain('resource not found'); 331 | done(); 332 | } 333 | ); 334 | }); 335 | 336 | // this is a 404 *at the end* of the traversal, which is *not* interpreted as 337 | // an error condition 338 | it('should just deliver the last response of get(), even when the last ' + 339 | 'response is a 404', 340 | function(done) { 341 | api 342 | .newRequest() 343 | .follow('blind_alley') 344 | .get() 345 | .result 346 | .then(successCallback, errorCallback); 347 | waitFor( 348 | function() { return successCallback.called; }, 349 | function() { 350 | var resultDoc = checkResponseWithBody(404); 351 | expect(resultDoc).to.exist; 352 | expect(resultDoc.message).to.exist; 353 | expect(resultDoc.message).to.equal('resource not found'); 354 | done(); 355 | } 356 | ); 357 | }); 358 | 359 | // same as above, that is, a 404 *at the end* of the traversal, which is 360 | // *not* interpreted as an error condition, this time using AngularJS' $http 361 | // service 362 | it('should just deliver the last response of get(), even when the last ' + 363 | 'response is a 404 (/w AngularJS\' $http)', 364 | function(done) { 365 | api 366 | .newRequest() 367 | .useAngularHttp() 368 | .follow('blind_alley') 369 | .get() 370 | .result 371 | .then(successCallback, errorCallback); 372 | waitFor( 373 | function() { return successCallback.called; }, 374 | function() { 375 | var resultDoc = checkResponseWithBody(404); 376 | expect(resultDoc).to.exist; 377 | expect(resultDoc.message).to.exist; 378 | expect(resultDoc.message).to.equal('resource not found'); 379 | done(); 380 | } 381 | ); 382 | }); 383 | 384 | /* 385 | it('https://github.com/traverson/traverson-angular/issues/14', 386 | function(done) { 387 | traversonAngular 388 | .from(rootUri) 389 | .useAngularHttp() 390 | .follow() 391 | .post({}) 392 | .result 393 | .then(successCallback, errorCallback); 394 | 395 | waitFor( 396 | function() { return successCallback.called || errorCallback.called; }, 397 | function() { 398 | console.log('successCallback.called', successCallback.called); 399 | console.log('errorCallback.called', errorCallback.called); 400 | done(); 401 | } 402 | ); 403 | }); 404 | */ 405 | 406 | // again, 404 during traversal => error, this time with getResouce() 407 | it('should fail gracefully on 404 during traversal (getResource())', 408 | function(done) { 409 | api 410 | .newRequest() 411 | .follow('blind_alley') 412 | .getResource().result.then(successCallback, errorCallback); 413 | waitFor( 414 | function() { return errorCallback.called; }, 415 | function() { 416 | expect(successCallback.callCount).to.equal(0); 417 | expect(errorCallback.callCount).to.equal(1); 418 | var error = errorCallback.firstCall.args[0]; 419 | expect(error).to.exist; 420 | expect(error.name).to.equal('HTTPError'); 421 | expect(error.message).to.equal('HTTP GET request to ' + rootUri + 422 | 'does/not/exist' + ' resulted in HTTP status code 404.'); 423 | expect(error.url).to.equal(rootUri + 'does/not/exist'); 424 | expect(error.httpStatus).to.equal(404); 425 | 426 | var lastBody = error.body; 427 | expect(lastBody).to.exist; 428 | expect(lastBody).to.contain('message'); 429 | expect(lastBody).to.contain('resource not found'); 430 | done(); 431 | } 432 | ); 433 | }); 434 | 435 | it('should fail gracefully on syntactically incorrect JSON', 436 | function(done) { 437 | traversonAngular 438 | .from(rootUri) 439 | .json() 440 | .follow('garbage') 441 | .getResource().result.then(successCallback, errorCallback); 442 | waitFor( 443 | function() { return errorCallback.called; }, 444 | function() { 445 | expect(successCallback.callCount).to.equal(0); 446 | expect(errorCallback.callCount).to.equal(1); 447 | var error = errorCallback.firstCall.args[0]; 448 | expect(error).to.exist; 449 | expect(error.name).to.equal('JSONError'); 450 | expect(error.message).to.equal('The document at ' + rootUri + 'junk' + 451 | ' could not be parsed as JSON: { this will :: not parse'); 452 | expect(error.url).to.equal(rootUri + 'junk'); 453 | expect(error.body).to.equal('{ this will :: not parse'); 454 | done(); 455 | } 456 | ); 457 | }); 458 | 459 | it('should abort a link traversal process and the current request', 460 | function(done) { 461 | var handle = 462 | api 463 | .newRequest() 464 | .follow('second', 'doc') 465 | .getResource(); 466 | 467 | handle.result.then(successCallback, errorCallback); 468 | handle.abort(); 469 | waitFor( 470 | function() { return errorCallback.called; }, 471 | function() { 472 | expect(successCallback.callCount).to.equal(0); 473 | expect(errorCallback.callCount).to.equal(1); 474 | var error = errorCallback.firstCall.args[0]; 475 | expect(error).to.exist; 476 | expect(error.message).to.equal( 477 | 'Link traversal process has been aborted.'); 478 | done(); 479 | } 480 | ); 481 | }); 482 | 483 | it('should abort a post request', 484 | function(done) { 485 | var handle = 486 | api 487 | .newRequest() 488 | .post({}); 489 | 490 | handle.result.then(successCallback, errorCallback); 491 | handle.abort(); 492 | waitFor( 493 | function() { return errorCallback.called; }, 494 | function() { 495 | expect(successCallback.callCount).to.equal(0); 496 | expect(errorCallback.callCount).to.equal(1); 497 | var error = errorCallback.firstCall.args[0]; 498 | expect(error).to.exist; 499 | expect(error.message).to.equal( 500 | 'Link traversal process has been aborted.'); 501 | done(); 502 | } 503 | ); 504 | }); 505 | 506 | it('should yield the last URI', function(done) { 507 | api 508 | .newRequest() 509 | .follow('second', 'doc') 510 | .getUrl() 511 | .result 512 | .then(successCallback, errorCallback); 513 | waitFor( 514 | function() { return successCallback.called; }, 515 | function() { 516 | expect(successCallback.callCount).to.equal(1); 517 | expect(errorCallback.callCount).to.equal(0); 518 | var result = successCallback.firstCall.args[0]; 519 | expect(result).to.exist; 520 | expect(result).to.equal(rootUri + 'second/document'); 521 | done(); 522 | } 523 | ); 524 | }); 525 | 526 | it('should post', function(done) { 527 | var payload = {'new': 'document'}; 528 | api 529 | .newRequest() 530 | .follow('post_link') 531 | .post(payload) 532 | .result 533 | .then(successCallback, errorCallback); 534 | waitFor( 535 | function() { return successCallback.called; }, 536 | function() { 537 | var resultDoc = checkResponseWithBody(201); 538 | expect(resultDoc.document).to.exist; 539 | expect(resultDoc.document).to.equal('created'); 540 | expect(resultDoc.received).to.exist; 541 | expect(resultDoc.received).to.deep.equal(payload); 542 | done(); 543 | } 544 | ); 545 | }); 546 | 547 | it('should put', function(done) { 548 | var payload = {'updated': 'document'}; 549 | api 550 | .newRequest() 551 | .follow('put_link') 552 | .put(payload) 553 | .result 554 | .then(successCallback, errorCallback); 555 | waitFor( 556 | function() { return successCallback.called; }, 557 | function() { 558 | var resultDoc = checkResponseWithBody(); 559 | expect(resultDoc.document).to.exist; 560 | expect(resultDoc.document).to.equal('overwritten'); 561 | expect(resultDoc.received).to.exist; 562 | expect(resultDoc.received).to.deep.equal(payload); 563 | done(); 564 | } 565 | ); 566 | }); 567 | 568 | // This test will not work via mocha-phantomjs since PhantomJS currently 569 | // sends an empty body with a PATCH request, see 570 | // https://github.com/ariya/phantomjs/issues/11384 571 | it.skip('should patch', function(done) { 572 | var payload = {'patched': 'document'}; 573 | api 574 | .newRequest() 575 | .follow('patch_link') 576 | .patch(payload) 577 | .result 578 | .then(successCallback, errorCallback); 579 | waitFor( 580 | function() { return successCallback.called; }, 581 | function() { 582 | var resultDoc = checkResponseWithBody(); 583 | expect(resultDoc.document).to.exist; 584 | expect(resultDoc.document).to.equal('patched'); 585 | expect(resultDoc.received).to.exist; 586 | expect(resultDoc.received).to.deep.equal(payload); 587 | done(); 588 | } 589 | ); 590 | }); 591 | 592 | it('should delete', function(done) { 593 | api 594 | .newRequest() 595 | .follow('delete_link') 596 | .delete() 597 | .result 598 | .then(successCallback, errorCallback); 599 | waitFor( 600 | function() { return successCallback.called; }, 601 | function() { 602 | var response = checkResponse(204); 603 | done(); 604 | } 605 | ); 606 | }); 607 | 608 | it('should use provided request options', function(done) { 609 | api 610 | .newRequest() 611 | .withRequestOptions({ 612 | headers: { 613 | 'Accept': 'application/json', 614 | 'X-Traverson-Test-Header': 'Traverson rocks!' 615 | } 616 | }) 617 | .follow('echo-headers') 618 | .getResource() 619 | .result 620 | .then(successCallback, errorCallback); 621 | waitFor( 622 | function() { return successCallback.called; }, 623 | function() { 624 | var resultDoc = checkResultDoc(); 625 | var testResponseHeader = 626 | resultDoc['X-Traverson-Test-Header'] || 627 | resultDoc['x-traverson-test-header']; 628 | expect(testResponseHeader).to.exist; 629 | expect(testResponseHeader).to.equal('Traverson rocks!'); 630 | done(); 631 | } 632 | ); 633 | }); 634 | 635 | it('should use provided request options (/w AngularJS\' $http)', 636 | function(done) { 637 | var payload = {'updated': 'document'}; 638 | api 639 | .newRequest() 640 | .follow('put_link') 641 | .useAngularHttp() 642 | .withRequestOptions({ 643 | headers: { 644 | 'Content-Type': 'application/json', 645 | 'If-Match': 'something' 646 | } 647 | }) 648 | .put(payload) 649 | .result 650 | .then(successCallback, errorCallback); 651 | waitFor( 652 | function() { return successCallback.called; }, 653 | function() { 654 | var resultDoc = checkResponseWithBody(); 655 | expect(resultDoc.document).to.exist; 656 | expect(resultDoc.document).to.equal('overwritten'); 657 | expect(resultDoc.received).to.exist; 658 | expect(resultDoc.received).to.deep.equal(payload); 659 | expect(resultDoc.headers).to.exist; 660 | expect(resultDoc.headers['content-type']).to.equal('application/json'); 661 | expect(resultDoc.headers['if-match']).to.equal('something'); 662 | done(); 663 | } 664 | ); 665 | }); 666 | 667 | it('should use provided query string options', function(done) { 668 | api 669 | .newRequest() 670 | .withRequestOptions({ 671 | qs: { 672 | 'token': 'foobar' 673 | } 674 | }) 675 | .follow('echo-query') 676 | .getResource() 677 | .result 678 | .then(successCallback, errorCallback); 679 | waitFor( 680 | function() { return successCallback.called; }, 681 | function() { 682 | var resultDoc = checkResultDoc(); 683 | expect(resultDoc.token).to.exist; 684 | expect(resultDoc.token).to.equal('foobar'); 685 | done(); 686 | } 687 | ); 688 | }); 689 | 690 | it('should add request options on top of each other', function(done) { 691 | api 692 | .newRequest() 693 | .addRequestOptions({ 694 | headers: { 'Accept': 'application/json', } 695 | }) 696 | .addRequestOptions({ 697 | headers: { 'X-Traverson-Test-Header': 'Traverson rocks!' } 698 | }) 699 | .addRequestOptions({ 700 | qs: { 'token': 'foobar' } 701 | }) 702 | .follow('echo-all') 703 | .getResource() 704 | .result 705 | .then(successCallback, errorCallback); 706 | waitFor( 707 | function() { return successCallback.called; }, 708 | function() { 709 | var resultDoc = checkResultDoc(); 710 | var responseAcceptHeader = 711 | resultDoc.headers.Accept || 712 | resultDoc.headers.accept; 713 | var responseTestHeader = 714 | resultDoc.headers['X-Traverson-Test-Header'] || 715 | resultDoc.headers['x-traverson-test-header']; 716 | expect(responseAcceptHeader).to.exist; 717 | expect(responseAcceptHeader).to.equal('application/json'); 718 | expect(responseTestHeader).to.exist; 719 | expect(responseTestHeader).to.equal('Traverson rocks!'); 720 | expect(resultDoc.query.token).to.equal('foobar'); 721 | done(); 722 | } 723 | ); 724 | }); 725 | 726 | it( 727 | 'should add request options on top of each other (/w AngularJS\' $http)', 728 | function(done) { 729 | api 730 | .newRequest() 731 | .useAngularHttp() 732 | .addRequestOptions({ 733 | headers: { 'Accept': 'application/json', } 734 | }) 735 | .addRequestOptions({ 736 | headers: { 'X-Traverson-Test-Header': 'Traverson rocks!' } 737 | }) 738 | .addRequestOptions({ 739 | qs: { 'token': 'foobar' } 740 | }) 741 | .follow('echo-all') 742 | .getResource() 743 | .result 744 | .then(successCallback, errorCallback); 745 | waitFor( 746 | function() { return successCallback.called; }, 747 | function() { 748 | var resultDoc = checkResultDoc(); 749 | var responseAcceptHeader = 750 | resultDoc.headers.Accept || 751 | resultDoc.headers.accept; 752 | var responseTestHeader = 753 | resultDoc.headers['X-Traverson-Test-Header'] || 754 | resultDoc.headers['x-traverson-test-header']; 755 | expect(responseAcceptHeader).to.exist; 756 | expect(responseAcceptHeader).to.equal('application/json'); 757 | expect(responseTestHeader).to.exist; 758 | expect(responseTestHeader).to.equal('Traverson rocks!'); 759 | expect(resultDoc.query.token).to.equal('foobar'); 760 | done(); 761 | } 762 | ); 763 | }); 764 | 765 | it('should use provided request options with post', function(done) { 766 | var payload = { what: 'ever' }; 767 | api 768 | .newRequest() 769 | .withRequestOptions({ 770 | headers: { 771 | 'Accept': 'application/json', 772 | 'Content-Type': 'application/json', 773 | 'X-Traverson-Test-Header': 'Traverson rocks!' 774 | }, 775 | qs: { 'token': 'foobar' } 776 | }) 777 | .follow('echo-all') 778 | .post(payload) 779 | .result 780 | .then(successCallback, errorCallback); 781 | waitFor( 782 | function() { return successCallback.called; }, 783 | function() { 784 | var resultDoc = checkResponseWithBody(201); 785 | var responseAcceptHeader = 786 | resultDoc.headers.Accept || 787 | resultDoc.headers.accept; 788 | var responseTestHeader = 789 | resultDoc.headers['X-Traverson-Test-Header'] || 790 | resultDoc.headers['x-traverson-test-header']; 791 | expect(responseAcceptHeader).to.exist; 792 | expect(responseAcceptHeader).to.equal('application/json'); 793 | expect(responseTestHeader).to.exist; 794 | expect(responseTestHeader).to.equal('Traverson rocks!'); 795 | expect(resultDoc.query.token).to.equal('foobar'); 796 | expect(resultDoc.received).to.exist; 797 | expect(resultDoc.received).to.deep.equal(payload); 798 | done(); 799 | } 800 | ); 801 | }); 802 | 803 | it('should post with x-www-form-urlencoded', 804 | function(done) { 805 | var payload = { item: '#4711', quantity: 1 }; 806 | traversonAngular 807 | .from(rootUri) 808 | .withRequestOptions([ 809 | { headers: { 'Accept': 'application/json' } }, 810 | { 811 | headers: { 812 | 'Accept': 'application/json', 813 | 'Content-Type': 'application/x-www-form-urlencoded', 814 | } 815 | } 816 | ]) 817 | .follow('echo-all') 818 | .post(payload).result.then(successCallback, errorCallback); 819 | waitFor( 820 | function() { return successCallback.called; }, 821 | function() { 822 | var resultDoc = checkResponseWithBody(201); 823 | var responseAcceptHeader = 824 | resultDoc.headers.Accept || 825 | resultDoc.headers.accept; 826 | var responseContentType = 827 | resultDoc.headers['Content-Type'] || 828 | resultDoc.headers['content-type']; 829 | expect(responseAcceptHeader).to.exist; 830 | expect(responseAcceptHeader).to.equal('application/json'); 831 | expect(responseContentType).to.exist; 832 | expect(responseContentType) 833 | .to.equal('application/x-www-form-urlencoded'); 834 | expect(resultDoc.received).to.exist; 835 | expect(JSON.stringify(resultDoc.received)).to.contain('item'); 836 | expect(JSON.stringify(resultDoc.received)).to.contain('#4711'); 837 | done(); 838 | } 839 | ); 840 | }); 841 | 842 | it('should post form via request options with x-www-form-urlencoded', 843 | function(done) { 844 | var order = { item: '#4711', quantity: '1'}; 845 | traversonAngular 846 | .from(rootUri) 847 | .withRequestOptions([ 848 | { headers: { 'Accept': 'application/json' } }, 849 | { 850 | headers: { 'Accept': 'application/json' }, 851 | form: order, 852 | } 853 | ]) 854 | .follow('echo-all') 855 | .post(null).result.then(successCallback, errorCallback); 856 | waitFor( 857 | function() { return successCallback.called; }, 858 | function() { 859 | var resultDoc = checkResponseWithBody(201); 860 | var responseAcceptHeader = 861 | resultDoc.headers.Accept || 862 | resultDoc.headers.accept; 863 | var responseContentType = 864 | resultDoc.headers['Content-Type'] || 865 | resultDoc.headers['content-type']; 866 | expect(responseAcceptHeader).to.exist; 867 | expect(responseAcceptHeader).to.equal('application/json'); 868 | expect(responseContentType).to.exist; 869 | expect(responseContentType) 870 | .to.equal('application/x-www-form-urlencoded'); 871 | expect(resultDoc.received).to.exist; 872 | expect(resultDoc.received).to.deep.equal(order); 873 | done(); 874 | } 875 | ); 876 | }); 877 | 878 | function checkResponseWithBody(httpStatus) { 879 | var response = checkResponse(httpStatus); 880 | var body = response.body; 881 | expect(body).to.exist; 882 | var resultDoc = JSON.parse(body); 883 | return resultDoc; 884 | } 885 | 886 | function checkResponse(httpStatus) { 887 | httpStatus = httpStatus || 200; 888 | expect(successCallback.callCount).to.equal(1); 889 | expect(errorCallback.callCount).to.equal(0); 890 | var response = successCallback.firstCall.args[0]; 891 | expect(response).to.exist; 892 | expect(response.statusCode).to.exist; 893 | expect(response.statusCode).to.equal(httpStatus); 894 | return response; 895 | } 896 | 897 | function checkResultDoc() { 898 | expect(successCallback.callCount).to.equal(1); 899 | expect(errorCallback.callCount).to.equal(0); 900 | var resultDoc = successCallback.firstCall.args[0]; 901 | expect(resultDoc).to.exist; 902 | return resultDoc; 903 | } 904 | }); 905 | -------------------------------------------------------------------------------- /test/using_angular_mocks.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 'use strict'; 3 | 4 | var chai = require('chai') 5 | , expect = chai.expect; 6 | 7 | describe('traverson-angular using angular-mocks', function () { 8 | 9 | var rootUri = 'http://api.example.org'; 10 | var httpBackend, traverson; 11 | 12 | beforeEach(angular.mock.module('traverson')); 13 | 14 | beforeEach(angular.mock.inject(function (_$httpBackend_, _traverson_) { 15 | traverson = _traverson_; 16 | 17 | httpBackend = _$httpBackend_; 18 | httpBackend.whenGET(rootUri).respond(JSON.stringify({ 19 | stuff: 'a value', 20 | link: rootUri + '/link/to/thing' 21 | })); 22 | httpBackend.whenGET(rootUri + '/link/to/thing').respond(JSON.stringify({ 23 | foo: 'bar' 24 | })); 25 | })); 26 | 27 | afterEach(function () { 28 | httpBackend.verifyNoOutstandingExpectation(); 29 | httpBackend.verifyNoOutstandingRequest(); 30 | }); 31 | 32 | it('should access the root URI', function (done) { 33 | traverson 34 | .from(rootUri) 35 | .useAngularHttp() 36 | .json() 37 | .newRequest() 38 | .getResource() 39 | .result 40 | .then(function (response) { 41 | expect(response.stuff).to.equal('a value'); 42 | done(); 43 | }, function (error) { 44 | done(error); 45 | }); 46 | 47 | httpBackend.flush(); 48 | }); 49 | 50 | it('should access the link', function (done) { 51 | traverson 52 | .from(rootUri) 53 | .useAngularHttp() 54 | .json() 55 | .newRequest() 56 | .follow('link') 57 | .getResource() 58 | .result 59 | .then(function (response) { 60 | expect(response.foo).to.equal('bar'); 61 | done(); 62 | }, function (error) { 63 | done(error); 64 | }); 65 | 66 | httpBackend.flush(); 67 | setTimeout(function() { 68 | httpBackend.flush(); 69 | }, 0); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // from node core util, copied here so the production code shim 4 | // browser/lib/shim/node-util.js does not need to have this. 5 | exports.inherits = function(ctor, superCtor) { 6 | ctor.super_ = superCtor; 7 | ctor.prototype = Object.create(superCtor.prototype, { 8 | constructor: { 9 | value: ctor, 10 | enumerable: false, 11 | writable: true, 12 | configurable: true 13 | } 14 | }); 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /traverson-angular.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 'use strict'; 3 | 4 | var traverson = require('traverson'); 5 | 6 | var ng; 7 | if (typeof angular !== 'undefined') { 8 | // angular is defined globally, use this 9 | ng = angular; 10 | } else { 11 | // angular is not defined globally, try to require it 12 | ng = require('angular'); 13 | if (typeof ng.module !== 'function') { 14 | throw new Error('angular has either to be provided globally or made ' + 15 | 'available as a shim for browserify. (Also, if the angular module on ' + 16 | 'npm would actually be a proper CommonJS module, this error ' + 17 | 'wouldn\'t be a thing.)'); 18 | } 19 | } 20 | 21 | var traversonAngular = ng.module('traverson', []); 22 | var Builder = traverson._Builder; 23 | var originalMethods = { 24 | get: Builder.prototype.get, 25 | getResource: Builder.prototype.getResource, 26 | getUrl: Builder.prototype.getUrl, 27 | post: Builder.prototype.post, 28 | put: Builder.prototype.put, 29 | patch: Builder.prototype.patch, 30 | delete: Builder.prototype.delete, 31 | }; 32 | 33 | traversonAngular.factory('traverson', 34 | ['$q', '$httpTraversonAdapter', 35 | function traversonFactory($q, $httpTraversonAdapter) { 36 | 37 | function promisify(that, originalMethod, argsArray) { 38 | var deferred = $q.defer(); 39 | 40 | argsArray = argsArray || []; 41 | 42 | var traversal; 43 | var callback = function(err, result, _traversal) { 44 | if (err) { 45 | err.result = result; 46 | deferred.reject(err); 47 | } else { 48 | traversal = _traversal; 49 | deferred.resolve(result); 50 | } 51 | }; 52 | 53 | argsArray.push(callback); 54 | 55 | var traversalHandler = originalMethod.apply(that, argsArray); 56 | 57 | function continueTraversal() { 58 | var deferredContinue = $q.defer(); 59 | deferred.promise.then(function() { 60 | deferredContinue.resolve(traversal.continue()); 61 | }, function() { 62 | var error = new Error('Can\'t continue from a broken traversal.'); 63 | error.name = 'InvalidStateError'; 64 | throw error; 65 | }); 66 | return deferredContinue.promise; 67 | } 68 | 69 | return { 70 | result: deferred.promise, 71 | continue: continueTraversal, 72 | abort: traversalHandler.abort, 73 | then: function() { 74 | throw new Error('As of version 2.0.0, Traverson\'s action methods ' + 75 | 'do no longer return the promise directly. Code like \n' + 76 | 'traverson.from(url).follow(...).getResource().then(...)\n' + 77 | 'needs to be changed to \n' + 78 | 'traverson.from(url).follow(...).getResource().result.then(...)'); 79 | }, 80 | }; 81 | } 82 | 83 | Builder.prototype.get = function() { 84 | return promisify(this, originalMethods.get); 85 | }; 86 | 87 | Builder.prototype.getResource = function() { 88 | return promisify(this, originalMethods.getResource); 89 | }; 90 | 91 | Builder.prototype.getUrl = Builder.prototype.getUri = function() { 92 | return promisify(this, originalMethods.getUrl); 93 | }; 94 | 95 | Builder.prototype.post = function(body) { 96 | return promisify(this, originalMethods.post, [body]); 97 | }; 98 | 99 | Builder.prototype.put = function(body) { 100 | return promisify(this, originalMethods.put, [body]); 101 | }; 102 | 103 | Builder.prototype.patch = function(body) { 104 | return promisify(this, originalMethods.patch, [body]); 105 | }; 106 | 107 | Builder.prototype.delete = Builder.prototype.del = function() { 108 | return promisify(this, originalMethods.delete); 109 | }; 110 | 111 | Builder.prototype.useAngularHttp = function() { 112 | this.withRequestLibrary($httpTraversonAdapter); 113 | return this; 114 | }; 115 | 116 | return traverson; 117 | }]); 118 | 119 | traversonAngular.factory('$httpTraversonAdapter', [ 120 | '$http', '$q', function $httpTraversonAdapterFactory($http, $q) { 121 | 122 | function Request() { } 123 | 124 | Request.prototype.get = function(uri, options, callback) { 125 | options = mapOptions(options); 126 | $http 127 | .get(uri, options) 128 | .then(handleResponse(callback)) 129 | .catch(handleError(callback)); 130 | return new AbortHandle(options.timeout); 131 | }; 132 | 133 | Request.prototype.post = function(uri, options, callback) { 134 | options = mapOptions(options); 135 | $http 136 | .post(uri, options.data, options) 137 | .then(handleResponse(callback)) 138 | .catch(handleError(callback)); 139 | return new AbortHandle(options.timeout); 140 | }; 141 | 142 | Request.prototype.put = function(uri, options, callback) { 143 | options = mapOptions(options); 144 | $http 145 | .put(uri, options.data, options) 146 | .then(handleResponse(callback)) 147 | .catch(handleError(callback)); 148 | return new AbortHandle(options.timeout); 149 | }; 150 | 151 | Request.prototype.patch = function(uri, options, callback) { 152 | options = mapOptions(options); 153 | $http 154 | .patch(uri, options.data, options) 155 | .then(handleResponse(callback)) 156 | .catch(handleError(callback)); 157 | return new AbortHandle(options.timeout); 158 | }; 159 | 160 | Request.prototype.del = function(uri, options, callback) { 161 | options = mapOptions(options); 162 | $http 163 | .delete(uri, options) 164 | .then(handleResponse(callback)) 165 | .catch(handleError(callback)); 166 | return new AbortHandle(options.timeout); 167 | }; 168 | 169 | function mapOptions(options) { 170 | options = options || {}; 171 | var mappedOptions = {}; 172 | mapQuery(mappedOptions, options); 173 | mapHeaders(mappedOptions, options); 174 | mapAuth(mappedOptions, options); 175 | mapBody(mappedOptions, options); 176 | mapForm(mappedOptions, options); 177 | // do not parse JSON automatically, this will trip up Traverson 178 | mappedOptions.transformResponse = function(data, headersGetter, status) { 179 | return data; 180 | }; 181 | // hook to abort the request, if necessary 182 | mappedOptions.timeout = $q.defer(); 183 | return mappedOptions; 184 | } 185 | 186 | function mapQuery(mappedOptions, options) { 187 | // options.qs would be correct since we are using request/request options 188 | // object API, but a previous version of traverson-angular incorrectly 189 | // used options.query instead, so we allow this also, to not break 190 | // backwards compatibility. 191 | var qs = options.qs || options.query; 192 | if (qs) { 193 | mappedOptions.params = qs; 194 | } 195 | } 196 | 197 | function mapHeaders(mappedOptions, options) { 198 | if (options.headers) { 199 | mappedOptions.headers = options.headers; 200 | } 201 | } 202 | 203 | function mapAuth(mappedOptions, options) { 204 | var auth = options.auth; 205 | if (auth) { 206 | var username = auth.user || auth.username; 207 | var password = auth.pass || auth.password; 208 | mappedOptions.headers = mappedOptions.headers || {}; 209 | mappedOptions.headers.Authorization = 'Basic ' + btoa(username + ':' + 210 | password); 211 | } 212 | } 213 | 214 | function mapBody(mappedOptions, options) { 215 | if (options.body) { 216 | mappedOptions.data = options.body; 217 | } 218 | } 219 | 220 | function mapForm(mappedOptions, options) { 221 | var form = options.form; 222 | if (form) { 223 | mappedOptions.data = form; 224 | mappedOptions.headers = mappedOptions.headers || {}; 225 | mappedOptions.headers['Content-Type'] = 226 | 'application/x-www-form-urlencoded'; 227 | } 228 | } 229 | 230 | function mapResponse(response) { 231 | response.body = response.data; 232 | response.headers = response.headers(); 233 | response.statusCode = response.status; 234 | return response; 235 | } 236 | 237 | function handleResponse(callback) { 238 | return function(response) { 239 | return callback(null, mapResponse(response)); 240 | }; 241 | } 242 | 243 | function handleError(callback) { 244 | return function(response) { 245 | if (response.status >= 100 && response.status < 600) { 246 | // This happens on a completed HTTP request with a status code outside 247 | // of the 2xx range. In the context of Traverson, this is not an 248 | // error, in particular, if this is the last request in a traversal. 249 | // Thus, we re-route it to the successCallback. Handling 4xx and 5xx 250 | // errors during the traversal is the responsibility of traverson, not 251 | // traverson-angular. 252 | return callback(null, mapResponse(response)); 253 | } else { 254 | // This happens on network errors, timeouts etc. In this case, 255 | // AngularJS sets the status property to 0. In the context of 256 | // Traverson, only these are to be interpreted as errors. 257 | return callback(response); 258 | } 259 | }; 260 | } 261 | 262 | return new Request(); 263 | } 264 | ]); 265 | 266 | function AbortHandle(abortPromise) { 267 | this.abortPromise = abortPromise; 268 | this.listeners = []; 269 | } 270 | 271 | AbortHandle.prototype.abort = function() { 272 | this.abortPromise.resolve(); 273 | this.listeners.forEach(function(fn) { 274 | fn.call(); 275 | }); 276 | }; 277 | 278 | AbortHandle.prototype.on = function(event, fn) { 279 | if (event !== 'abort') { 280 | var error = new Error('Event ' + event + ' not supported'); 281 | error.name = 'InvalidArgumentError'; 282 | throw error; 283 | } 284 | this.listeners.push(fn); 285 | }; 286 | 287 | module.exports = traversonAngular; 288 | --------------------------------------------------------------------------------