├── .all-contributorsrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ └── new-issue-template.md └── workflows │ ├── issues_to_project.yml │ ├── pr_to_project.yml │ ├── publish_validator.yml │ └── unit_tests.yaml ├── .gitignore ├── .gitmodules ├── .nvmrc ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RULES.md ├── check-systems ├── README.md ├── index.js └── package.json ├── common └── http-utils.js ├── docs ├── SwaggerUI │ ├── README.md │ ├── dist │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── index.css │ │ ├── oauth2-redirect.html │ │ ├── swagger-initializer.js │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui-bundle.js.map │ │ ├── swagger-ui-es-bundle-core.js │ │ ├── swagger-ui-es-bundle-core.js.map │ │ ├── swagger-ui-es-bundle.js │ │ ├── swagger-ui-es-bundle.js.map │ │ ├── swagger-ui-standalone-preset.js │ │ ├── swagger-ui-standalone-preset.js.map │ │ ├── swagger-ui.css │ │ ├── swagger-ui.css.map │ │ ├── swagger-ui.js │ │ └── swagger-ui.js.map │ ├── index.html │ ├── screenshots │ │ └── swagger-github-pages.png │ └── swagger-ui.version ├── gbfs-validator.yaml ├── local-test.md ├── local-test1.png ├── local-test2.png └── update-json-schema.md ├── functions ├── feed.js ├── package.json ├── validator-summary.js └── validator.js ├── gbfs-validator ├── README.md ├── __test__ │ ├── __snapshots__ │ │ └── gbfs.test.js.snap │ ├── cli.test.js │ ├── fixtures │ │ ├── conditional_default_reserve_time.js │ │ ├── conditionnal_no_vehicle_type_id.js │ │ ├── conditionnal_vehicle_type_id.js │ │ ├── conditionnal_vehicle_types_available.js │ │ ├── missing_vehicle_types.js │ │ ├── plan_id.js │ │ ├── server.js │ │ └── v3.0 │ │ │ ├── default.js │ │ │ └── exaustive.js │ ├── gbfs.test.js │ ├── gbfs.v3.0.test.js │ └── index.test.js ├── cli.js ├── gbfs.js ├── index.js ├── package.json ├── validate.js └── versions │ ├── README.md │ ├── partials │ ├── v2.1 │ │ ├── free_bike_status │ │ │ └── required_vehicle_type_id.js │ │ ├── station_status │ │ │ └── required_vehicle_types_available.js │ │ └── system_information │ │ │ └── required_store_uri.js │ ├── v2.2 │ │ ├── free_bike_status │ │ │ └── required_vehicle_type_id.js │ │ ├── station_status │ │ │ └── required_vehicle_types_available.js │ │ └── system_information │ │ │ └── required_store_uri.js │ ├── v2.3 │ │ ├── free_bike_status │ │ │ └── required_vehicle_type_id.js │ │ ├── station_status │ │ │ └── required_vehicle_types_available.js │ │ ├── system_information │ │ │ └── required_store_uri.js │ │ └── vehicle_types │ │ │ └── pricing_plan_id.js │ ├── v3.0 │ │ ├── station_status │ │ │ └── required_vehicle_types_available.js │ │ ├── system_information │ │ │ └── required_store_uri.js │ │ ├── vehicle_status │ │ │ └── required_vehicle_type_id.js │ │ └── vehicle_types │ │ │ └── pricing_plan_id.js │ └── v3.1-RC │ │ ├── station_status │ │ └── required_vehicle_types_available.js │ │ ├── system_information │ │ └── required_store_uri.js │ │ ├── vehicle_status │ │ └── required_vehicle_type_id.js │ │ └── vehicle_types │ │ ├── default_reserve_time_require.js │ │ └── pricing_plan_id.js │ ├── v1.0.js │ ├── v1.1.js │ ├── v2.0.js │ ├── v2.1.js │ ├── v2.2.js │ ├── v2.3.js │ ├── v3.0.js │ └── v3.1-RC.js ├── netlify.toml ├── package.json ├── renovate.json ├── webpack.dev.config.js ├── website ├── .env.exemple ├── .gitignore ├── README.md ├── index.html ├── package.json ├── public │ ├── _redirects │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── gbfs_validator.jpg │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── src │ ├── App.vue │ ├── components │ │ ├── DownloadSchema.vue │ │ ├── Footer.vue │ │ ├── GeofencingZonePopup.vue │ │ ├── Result.vue │ │ ├── StationPopup.vue │ │ ├── SubResult.vue │ │ └── VehiclePopup.vue │ ├── main.js │ ├── pages │ │ ├── Home.vue │ │ ├── Validator.vue │ │ └── Visualization.vue │ └── router.js └── vite.config.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "gbfs-validator", 3 | "projectOwner": "MobilityData", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": ["README.md"], 7 | "commit": false, 8 | "imageSize": 100, 9 | "contributorsPerLine": 7, 10 | "contributorsSortAlphabetically": true, 11 | "skipCi": true, 12 | "contributors": [] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-prettier/skip-formatting' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Issue Template 3 | about: 'Please use this template for all new issues.' 4 | --- 5 | 6 | ### If you are new to the GBFS Validator, please introduce yourself (name and organization/link to GBFS). It’s helpful to know who we're chatting with! 7 | 8 | ### What is the issue and _why_ is it an issue? 9 | 10 | ### Please describe some potential solutions you have considered (even if they aren’t related to GBFS). 11 | -------------------------------------------------------------------------------- /.github/workflows/issues_to_project.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.4.1 14 | with: 15 | # You can target a repository in a different organization 16 | # to the issue 17 | project-url: https://github.com/orgs/MobilityData/projects/58 18 | github-token: ${{ secrets.GBFS_REPOS_CURRENT_STATUS }} 19 | -------------------------------------------------------------------------------- /.github/workflows/pr_to_project.yml: -------------------------------------------------------------------------------- 1 | name: Add PRs to project 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.4.1 14 | with: 15 | # You can target a repository in a different organization to the issue 16 | project-url: https://github.com/orgs/MobilityData/projects/58 17 | github-token: ${{ secrets.GBFS_REPOS_CURRENT_STATUS }} 18 | -------------------------------------------------------------------------------- /.github/workflows/publish_validator.yml: -------------------------------------------------------------------------------- 1 | name: GBFS Validator Package - Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | check-versions: 10 | name: check-version-job 11 | runs-on: ubuntu-latest 12 | outputs: 13 | has-version-changed: ${{ steps.version-change-check.outputs.VERSION_CHANGED }} 14 | defaults: 15 | run: 16 | working-directory: ./gbfs-validator 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Get current published version 23 | id: get_current_published_version 24 | run: echo "VERSION=$(npm info gbfs-validator version)" >> $GITHUB_OUTPUT 25 | 26 | - name: Get current local version 27 | id: get_current_local_version 28 | run: echo "VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT 29 | 30 | - name: Check if version changed 31 | id: version-change-check 32 | env: 33 | CURRENT_VERSION: ${{ steps.get_current_published_version.outputs.VERSION }} 34 | LOCAL_VERSION: ${{ steps.get_current_local_version.outputs.VERSION }} 35 | run: | 36 | if [ "$CURRENT_VERSION" != "$LOCAL_VERSION" ]; then 37 | echo "Version changed from $CURRENT_VERSION to $LOCAL_VERSION" 38 | echo "VERSION_CHANGED=true" >> $GITHUB_OUTPUT 39 | else 40 | echo "Version did not change" 41 | echo "VERSION_CHANGED=false" >> $GITHUB_OUTPUT 42 | fi 43 | 44 | build-publish: 45 | name: build-publish-job 46 | needs: [check-versions] 47 | if: needs.check-versions.outputs.has-version-changed == 'true' 48 | runs-on: ubuntu-latest 49 | defaults: 50 | run: 51 | working-directory: ./gbfs-validator 52 | 53 | steps: 54 | - name: Checkout repository 55 | uses: actions/checkout@v2 56 | with: 57 | submodules: true 58 | fetch-depth: 0 59 | 60 | - name: Setup Node.js 61 | uses: actions/setup-node@v2 62 | with: 63 | node-version: '18' 64 | registry-url: 'https://registry.npmjs.org' 65 | 66 | - name: Install dependencies 67 | run: yarn 68 | 69 | - name: Load secrets from 1Password 70 | uses: 1password/load-secrets-action@v2.0.0 71 | with: 72 | export-env: true # Export loaded secrets as environment variables 73 | env: 74 | OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} 75 | NODE_AUTH_TOKEN: "op://rbiv7rvkkrsdlpcrz3bmv7nmcu/ppzc4jxrwkf3omdmcs7z2wiwum/credential" 76 | 77 | - name: Publish to npm 78 | run: npm publish 79 | env: 80 | NODE_AUTH_TOKEN: ${{ env.NODE_AUTH_TOKEN }} 81 | 82 | -------------------------------------------------------------------------------- /.github/workflows/unit_tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: push 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3.4.0 11 | 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | 16 | - name: setup node-gyp 17 | run: npm install -g node-gyp@latest 18 | 19 | - name: Run tests 20 | run: yarn install && yarn workspaces run test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | 16 | *.log 17 | coverage 18 | 19 | # Netlify build folders and files 20 | .netlify 21 | 22 | functions/*.zip 23 | 24 | .env 25 | 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gbfs-validator/versions/gbfs-json-schema"] 2 | path = gbfs-validator/versions/gbfs-json-schema 3 | url = https://github.com/MobilityData/gbfs-json-schema.git 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We welcome contributions to the project! 2 | 3 | # How to contribute to the project? 4 | 5 | All contributions to this project are welcome. To propose changes, we encourage contributors to: 6 | 7 | 1. Fork this project on GitHub 8 | 2. Create a new branch 9 | 3. Propose changes by opening a new pull request. 10 | If you're looking for somewhere to start, check out the issues labeled "Good first issue" or Community. 11 | 12 | # Issue and PR templates 13 | 14 | We encourage contributors to format pull request titles following the [Conventional Commit Specification](https://www.conventionalcommits.org/en/v1.0.0/). 15 | 16 | # Folder organization 17 | 18 | - gbfs-validator 19 | 20 | This is the heart of the validator. This folder contains a NodeJs package to validate GBFS Feeds. 21 | 22 | - gbfs-validator/schema 23 | 24 | Contains JSON schemas 25 | 26 | - website 27 | 28 | Contains the frontend, currently hosted by Netlify on https://gbfs-validator.mobilitydata.org/ 29 | It’s a tiny Vue SPA. 30 | 31 | - functions 32 | 33 | The API for the website uses a “lambda function”. 34 | This folder contains the lambda function. The function will depend on the gbfs-validator package. 35 | The function is only compatible with Netlify Function (https://www.netlify.com/products/functions/) for now. 36 | 37 | - check-systems 38 | 39 | Check-systems is a CLI tool to validate the whole “systems.csv” from https://github.com/MobilityData/gbfs locally 40 | 41 | # Code convention 42 | 43 | "Sticking to a single consistent and documented coding style for this project is important to ensure that code reviewers dedicate their attention to the functionality of the validation, as opposed to disagreements about the coding style (and avoid bike-shedding https://en.wikipedia.org/wiki/Law_of_triviality )." This project uses the Eslint + Prettier to ensure lint (See .eslintrc.js and .prettierrc) 44 | 45 | # Adding a new version 46 | 47 | For adding a new version: 48 | 49 | - Create a new folder under “gbfs-validator/schema” with the version as name (Eg: “vX.Y”). 50 | - Add an “index.js” file. This file will define the possible JSON-schema to call for validation and the mandatory ones. See [master/gbfs-validator/schema/v2.2/index.js](https://github.com/fluctuo/gbfs-validator/blob/master/gbfs-validator/schema/v2.2/index.js) for an exemple. 51 | - Fill the folder with all JSON-schemas for this version. 52 | - Add an item on this array: [master/website/src/components/Validator.vue#L98](https://github.com/MobilityData/gbfs-validator/blob/master/website/src/components/Validator.vue#L98) with the new version to be made available on the website. 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GBFS-validator 2 | 3 | [![All Contributors](https://img.shields.io/github/all-contributors/MobilityData/gbfs-validator?color=blue&style=flat)](#contributors) 4 | 5 | A [General Bikeshare Feed Specification](https://github.com/MobilityData/gbfs) dataset validator 6 | 7 | ## Introduction 8 | 9 | The Canonical GBFS Validator is a tool to check the conformity of a GBFS feed against the [official specification](https://github.com/MobilityData/gbfs/blob/master/gbfs.md). 10 | It validates feeds up to GBFS version 3.0. 11 | This tool is built using the [JSON Schemas](https://github.com/MobilityData/gbfs-json-schema), and the site is powered by [Netlify](https://www.netlify.com/). 12 | 13 | ![interface](https://github.com/MobilityData/gbfs-validator/assets/2423604/11206e7a-dd64-4133-bb32-eaa391815e60) 14 | 15 | The schemas in `gbfs-validator/versions/gbfs-json-schema` is a git submodule of https://github.com/MobilityData/gbfs-json-schema. 16 | 17 | Questions? Please open an issue or reach out on the #gbfs channel on the [MobilityData Slack](https://mobilitydata-io.slack.com/). 18 | 19 | ## Run the app 20 | 21 | The validator is developed to be used “online” (hosted with a lambda function). 22 | 23 | 1. Open https://gbfs-validator.mobilitydata.org 24 | 2. Enter the feed’s auto-discovery URL 25 | 3. If needed, select the version. If not specified, the validator will pick the version mentioned in the `gbfs.json` file 26 | 4. Select file requirement options (free-floating or docked) 27 | 5. Click the “Validate me !” button, and see the validation results below 28 | 29 | ## Validation rules 30 | 31 | The validation rules are listed in [RULES.md](/RULES.md) 32 | Have a suggestion for a new rule? Open an issue! 33 | 34 | ## Build the project: Web server install procedure 35 | 36 | ### Required 37 | 38 | To build the project locally, you need: 39 | 40 | - [Node.js](https://nodejs.org/en/download/). Minimum Node.js version `v14.x.x`, or higher. Recommend Node.js version `v18.x.x`. 41 | ```shell 42 | brew install node 43 | ``` 44 | 45 | - [Yarn](https://classic.yarnpkg.com/en/docs/install/) 46 | ```shell 47 | npm install --global yarn 48 | ``` 49 | 50 | - Ports 8080, 9000 and 9229 need to be free 51 | 52 | ### Run dev environment 53 | 54 | - Download or clone the repository 55 | ```shell 56 | git clone https://github.com/MobilityData/gbfs-validator.git 57 | cd gbfs-validator 58 | ``` 59 | 60 | - Install the requirements 61 | ```shell 62 | yarn 63 | ``` 64 | 65 | - Connect your local project to the `gbfs-validator` Netlify site to access its environment variables 66 | ```shell 67 | netlify link 68 | ``` 69 | Select `Enter the site name` and enter `gbfs-validator` 70 | 71 | - Run dev environment locally 72 | ```shell 73 | yarn run dev 74 | ``` 75 | 76 | - Open `localhost:8080` on your browser 77 | 78 | ### Command line 79 | The GBFS validator can be used as a Command Line Interface (CLI): 80 | 81 | - Download or clone the repository 82 | ```shell 83 | git clone https://github.com/MobilityData/gbfs-validator.git 84 | cd gbfs-validator 85 | ``` 86 | 87 | - Execute the CLI script 88 | ```shell 89 | node ./gbfs-validator/cli.js -u {http_address_of_gbfs_dataset} -s {local_path_to_output_report_file} 90 | ``` 91 | 92 | - To get the list of supported paramters 93 | ```shell 94 | node ./gbfs-validator/cli.js --help 95 | ``` 96 | 97 | - Usage description and supported parameters 98 | ``` 99 | Usage: cli [OPTIONS]... 100 | 101 | Options: 102 | -v, --version output the version number 103 | -u, --url URL of the GBFS feed 104 | -vb, --verbose Verbose mode prints debugging console logs 105 | -s, --save-report Local path to output report file 106 | -pr, --print-report Print report to standard output (choices: "yes", "no", default: "yes") 107 | -h, --help display help for command 108 | ``` 109 | 110 | ### Npm package 111 | The gbfs-validator Node.js npm package is now accessible for integration into your projects. To learn how to install and utilize it effectively, please refer to the [README](./gbfs-validator/README.md) for comprehensive guidance. 112 | 113 | ## Projects based on this validator 114 | 115 | [transport.data.gouv.fr GBFS validator tool](https://transport.data.gouv.fr/validation?type=gbfs) - Tool displaying interactive geofencing, station, and vehicle maps, the validation results, and metadata of GBFS feeds. 116 | 117 | ## Contributors 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | This project follows the [all-contributors](https://allcontributors.org/docs/en/overview) specification, find the [emoji key here](https://allcontributors.org/docs/en/emoji-key). Contributions of any kind welcome! Please check out our [Contribution guidelines](/CONTRIBUTING.md) for details. 129 | 130 | :warning: for contributions on schemas, please see [Versions README](gbfs-validator/versions/README.md) 131 | 132 | ## OpenAPI Specification 133 | 134 | :warning: **Subject to change**: This OpenAPI specification may change at any time. We do not recommend building any production systems that depend on this API directly. 135 | 136 | The OpenAPI specification can be viewed at: https://mobilitydata.github.io/gbfs-validator/SwaggerUI/index.html. 137 | 138 | ## Acknowledgments 139 | 140 | This project was originally created by Pierrick Paul ([@PierrickP](https://github.com/PierrickP)) at [fluctuo](https://fluctuo.com/) - MobilityData started maintaining the project in September 2021. 141 | -------------------------------------------------------------------------------- /RULES.md: -------------------------------------------------------------------------------- 1 | This project validates feeds up to version 3.1-RC of the [JSON Schemas](https://github.com/MobilityData/gbfs-json-schema). 2 | # Files presence 3 | The validator will flag any missing file. It will inform the user if the missing file is required or not, as per the conditions in the GBFS version that it detects. 4 | 5 | Screen Shot 2022-03-01 at 10 10 07 AM 6 | 7 | ## Required files 8 | `system_information.json` is **required** for all GBFS versions. 9 | `gbfs.json` is required as of v2.0 10 | 11 | ## Conditionally required files 12 | Three files are **conditionally required** for all GBFS versions: 13 | - `station_information.json`: required for systems utilizing docks 14 | - `station_status.json`: required for systems utilizing docks 15 | - `free_bike_status.json`: required for free floating vehicles 16 | The validator will check for the presence of the files depending on the options “Free-floating” or Docked” that the user selected on the interface. 17 | 18 | tick boxes 19 | 20 | 21 | The Validator also checks the conditional requirement of the file `vehicle_types.json`: as per the [official GBFS specification](https://github.com/MobilityData/gbfs/blob/master/gbfs.md#vehicle_typesjson), it is required of systems that include information about vehicle types in the `vehicle_status.json file`. 22 | 23 | 24 | # Fields presence and field types 25 | ## Required fields 26 | Each file in GBFS has to be structured in a specific [output format](https://github.com/MobilityData/gbfs/blob/master/gbfs.md#output-format). 27 | All the fields that are described as **required** in GBFS will be checked by the validator. 28 | Some fields are required only if the parent field is defined, and this is considered a **conditionally required** field. 29 | 30 | ## Conditionally Required fields 31 | The simple **conditionally required** fields (where the condition depends on another field in the same file) are represented by the JSON Schemas and will be checked by this validator.\ 32 | The more complex **conditionally required fields** are covered by custom rules that have been added in this validator (in [PR#63](https://github.com/MobilityData/gbfs-validator/pull/63)).\ 33 | 34 | The following conditions are all covered by this validator: 35 | - **system_information.json** 36 | 37 | `brand_assets.brand_last_modified`\ 38 | `brand_assets.brand_image_url`\ 39 | `terms_last_updated`\ 40 | `privacy_last_updated`\ 41 | `rental_apps.android.store_uri`\ 42 | `rental_apps.android.discovery_uri`\ 43 | `rental_apps.ios.store_uri`\ 44 | `rental_apps.ios.discovery_uri` 45 | 46 | - **vehicle_types.json** 47 | 48 | `vehicle_types.max_range_meters`\ 49 | `vehicle_types.vehicle_assets.icon_url`\ 50 | `vehicle_types.vehicle_assets.icon_last_modified`\ 51 | `default_pricing_plan_id` 52 | 53 | - **station_status.json** 54 | 55 | `stations.vehicle_types_available.vehicle_type_id`\ 56 | `stations.vehicle_types_available.count`\ 57 | `stations.vehicle_docks_available.vehicle_type_ids`\ 58 | `stations.vehicle_docks_available.count`\ 59 | `vehicle_types_available` 60 | 61 | - **geofencing_zones.json** 62 | 63 | `geofencing_zones.features.properties.rules.ride_allowed`\ 64 | `geofencing_zones.features.properties.rules.ride_through_allowed` 65 | 66 | - **free_bike_status.json** 67 | `vehicle_type_id` 68 | `current_range_meters` 69 | 70 | - **conditions that are not covered by this validator** 71 | `num_docks_available` in station_status.json`: because it depends on something that isn't defined in the GBFS files: the docking capacity. See the official GBFS spec about this field [here](https://github.com/MobilityData/gbfs/blob/master/gbfs.md#station_statusjson).\ 72 | `vehicle_docks_available` in station_status.json`: because it depends on something that isn't defined in the GBFS files: *REQUIRED in feeds where [...] certain docks are only able to accept certain vehicle types.* See the official GBFS spec about this field [here](https://github.com/MobilityData/gbfs/blob/master/gbfs.md#station_statusjson).\ 73 | `system_id` in `free_bike_status.json` 74 | 75 | ## Field types 76 | Each field has a specific field type, as described in the specification. 77 | The validators will flag the following field type if they are invalid. 78 | - array 79 | - boolean 80 | - date: defined in regex using the formula ```^[0-9]{4}-[0-9]{2}-[0-9]{2}$``` 81 | - email 82 | - enum 83 | - float 84 | - language: defined in regex with the formula ```^[a-z]{2,3}(-[A-Z]{2})?$``` 85 | - latitude: defined as number with a minimum of -90 and maximum of 90 86 | - longitude: defined as number with a minimum of -180 and maximum of 180 87 | - non-negative Float: defined as number 88 | - non-negative Integer: defined as number 89 | - object 90 | - string 91 | - time: defined in regex with the formula ```^([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$``` 92 | - timestamp: Defined as integer, with minimum set to Tuesday, December 15, 2015 5:00:00 AM (when GBFS was created) 93 | - url 94 | 95 | See examples for wrong field types below: 96 | 97 | 98 | enum 99 | type 100 | 101 | -------------------------------------------------------------------------------- /check-systems/README.md: -------------------------------------------------------------------------------- 1 | # check-systems 2 | 3 | Download systems.csv from https://github.com/MobilityData/gbfs 4 | 5 | `node check-systems/index.js systems.csv` 6 | -------------------------------------------------------------------------------- /check-systems/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const gbfsValidator = require('gbfs-validator') 4 | const parse = require('csv-parse') 5 | 6 | if (!process.argv[2]) { 7 | console.error('Usage: check-systems FILE') 8 | process.exit(1) 9 | } 10 | 11 | /** 12 | * This function reads the local version of the file systems.csv (must be downloaded manually first), 13 | * validates every system in the file and writes the validation report locally. 14 | * @param {*} line - A row in the file systems.csv. 15 | * @returns 16 | */ 17 | function checkGBFS(line) { 18 | const gbfs = new gbfsValidator(line[5]) 19 | 20 | return gbfs 21 | .validation() 22 | .then(result => { 23 | if (result.summary.versionUnimplemented) { 24 | console.log(`${line[3]} PASS (Version not implemented)`) 25 | } else { 26 | if (result.summary.hasErrors) { 27 | console.log(`${line[3]} KO (${result.summary.errorsCount} errors)`) 28 | fs.writeFileSync(`out-${line[3]}.log`, JSON.stringify(result, ' ', 2)) 29 | } else { 30 | console.log(`${line[3]} OK`) 31 | } 32 | } 33 | }) 34 | .catch(err => { 35 | console.log(line[3], 'ERROR', err.message) 36 | }) 37 | } 38 | 39 | fs.readFile(path.resolve(__dirname, process.argv[2]), (err, file) => { 40 | if (err) { 41 | console.error(err) 42 | process.exit(1) 43 | } 44 | 45 | parse(file, function(err, output) { 46 | Promise.all(output.slice(1).map(checkGBFS)) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /check-systems/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check-systems", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 0" 7 | }, 8 | "author": "Pierrick ", 9 | "main": "index.js", 10 | "license": "MIT", 11 | "dependencies": { 12 | "csv-parse": "^4.4.6" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/http-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Headers to be used on OPTIONS requests and responses 3 | */ 4 | const corsHeaders = { 5 | 'Access-Control-Allow-Origin': '*', 6 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 7 | 'Access-Control-Allow-Headers': '*', 8 | 'Access-Control-Request-Headers': '*' 9 | } 10 | 11 | /** 12 | * Default application response headers 13 | */ 14 | const defaultApplicationResponseHeaders = { 15 | ...corsHeaders, 16 | 'Content-Type': 'application/json' 17 | } 18 | 19 | /** 20 | * 21 | * @param event associated with the HTTP request. Example: GET, OPTIONS 22 | * @returns the CORS response for event equal OPTIONS, undefined otherwise 23 | */ 24 | const getCorsResponse = (event) => { 25 | if (event.httpMethod === 'OPTIONS') { 26 | return { 27 | statusCode: 200, 28 | headers: corsHeaders 29 | } 30 | } 31 | return undefined 32 | } 33 | 34 | module.exports = { 35 | corsHeaders, 36 | defaultApplicationResponseHeaders, 37 | getCorsResponse, 38 | } -------------------------------------------------------------------------------- /docs/SwaggerUI/README.md: -------------------------------------------------------------------------------- 1 | # How to host Swagger API documentation with GitHub Pages 2 | [The blog of Peter Evans: How to Host Swagger Documentation With Github Pages](https://peterevans.dev/posts/how-to-host-swagger-docs-with-github-pages/) 3 | 4 | This repository is a template for using the [Swagger UI](https://github.com/swagger-api/swagger-ui) to dynamically generate beautiful documentation for your API and host it for free with GitHub Pages. 5 | 6 | The template will periodically auto-update the Swagger UI dependency and create a pull request. See the [GitHub Actions workflow here](.github/workflows/update-swagger.yml). 7 | 8 | The example API specification used by this repository can be seen hosted at [https://peter-evans.github.io/swagger-github-pages](https://peter-evans.github.io/swagger-github-pages/). 9 | 10 | ## Steps to use this template 11 | 12 | 1. Click the `Use this template` button above to create a new repository from this template. 13 | 14 | 2. Go to the settings for your repository at `https://github.com/{github-username}/{repository-name}/settings` and enable GitHub Pages. 15 | 16 | ![Headers](/screenshots/swagger-github-pages.png?raw=true) 17 | 18 | 3. Browse to the Swagger documentation at `https://{github-username}.github.io/{repository-name}/`. 19 | 20 | 21 | ## Steps to manually configure in your own repository 22 | 23 | 1. Download the latest stable release of the Swagger UI [here](https://github.com/swagger-api/swagger-ui/releases). 24 | 25 | 2. Extract the contents and copy the "dist" directory to the root of your repository. 26 | 27 | 3. Move the file "index.html" from the directory "dist" to the root of your repository. 28 | ``` 29 | mv dist/index.html . 30 | ``` 31 | 32 | 4. Copy the YAML specification file for your API to the root of your repository. 33 | 34 | 5. Edit [dist/swagger-initializer.js](dist/swagger-initializer.js) and change the `url` property to reference your local YAML file. 35 | ```javascript 36 | window.ui = SwaggerUIBundle({ 37 | url: "swagger.yaml", 38 | ... 39 | ``` 40 | Then fix any references to files in the "dist" directory. 41 | ```html 42 | ... 43 | 44 | 45 | 46 | ... 47 | 48 | 49 | ... 50 | ``` 51 | 52 | 6. Go to the settings for your repository at `https://github.com/{github-username}/{repository-name}/settings` and enable GitHub Pages. 53 | 54 | ![Headers](/screenshots/swagger-github-pages.png?raw=true) 55 | 56 | 7. Browse to the Swagger documentation at `https://{github-username}.github.io/{repository-name}/`. 57 | 58 | The example API specification used by this repository can be seen hosted at [https://peter-evans.github.io/swagger-github-pages](https://peter-evans.github.io/swagger-github-pages/). 59 | -------------------------------------------------------------------------------- /docs/SwaggerUI/dist/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/docs/SwaggerUI/dist/favicon-16x16.png -------------------------------------------------------------------------------- /docs/SwaggerUI/dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/docs/SwaggerUI/dist/favicon-32x32.png -------------------------------------------------------------------------------- /docs/SwaggerUI/dist/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: -moz-scrollbars-vertical; 4 | overflow-y: scroll; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | background: #fafafa; 16 | } 17 | -------------------------------------------------------------------------------- /docs/SwaggerUI/dist/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI: OAuth2 Redirect 5 | 6 | 7 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /docs/SwaggerUI/dist/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | // 3 | 4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container 5 | window.ui = SwaggerUIBundle({ 6 | url: "../gbfs-validator.yaml", 7 | dom_id: '#swagger-ui', 8 | deepLinking: true, 9 | presets: [ 10 | SwaggerUIBundle.presets.apis, 11 | SwaggerUIStandalonePreset 12 | ], 13 | plugins: [ 14 | SwaggerUIBundle.plugins.DownloadUrl 15 | ], 16 | layout: "StandaloneLayout" 17 | }); 18 | 19 | // 20 | }; 21 | -------------------------------------------------------------------------------- /docs/SwaggerUI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/SwaggerUI/screenshots/swagger-github-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/docs/SwaggerUI/screenshots/swagger-github-pages.png -------------------------------------------------------------------------------- /docs/SwaggerUI/swagger-ui.version: -------------------------------------------------------------------------------- 1 | v4.19.0 2 | -------------------------------------------------------------------------------- /docs/gbfs-validator.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: MobilityData GBFS Validator 4 | version: 1.0.1 5 | contact: 6 | name: MobilityData 7 | url: https://mobilitydata.org/ 8 | email: api@mobilitydata.org 9 | license: 10 | name: MobilityData License 11 | url: https://www.apache.org/licenses/LICENSE-2.0 12 | servers: 13 | - url: https://gbfs-validator.netlify.app/.netlify/functions 14 | description: Production release environment 15 | - url: http://localhost:8888/.netlify/functions 16 | description: Local development environment 17 | paths: 18 | /validator: 19 | post: 20 | summary: Validate a GBFS feed 21 | description: Validate the GBFS feed according to passed options 22 | requestBody: 23 | content: 24 | application/json: 25 | schema: 26 | $ref: '#/components/schemas/ValidatorRequest' 27 | required: true 28 | responses: 29 | '200': 30 | description: Validation result 31 | content: 32 | application/json: 33 | schema: 34 | $ref: '#/components/schemas/Validator' 35 | '500': 36 | description: Error 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/Error' 41 | /feed: 42 | post: 43 | summary: Get feed content 44 | description: Get content of all GBFS's files. Useful to avoid CORS errors. 45 | requestBody: 46 | content: 47 | application/json: 48 | schema: 49 | $ref: '#/components/schemas/FeedRequest' 50 | required: true 51 | responses: 52 | '200': 53 | description: The feed content 54 | content: 55 | application/json: 56 | schema: 57 | $ref: '#/components/schemas/Feed' 58 | '500': 59 | description: Error 60 | content: 61 | application/json: 62 | schema: 63 | $ref: '#/components/schemas/Error' 64 | /validator-summary: 65 | post: 66 | summary: Get a summary of the validation results 67 | description: Returns a summary from the validator's response, including grouped error details. 68 | requestBody: 69 | content: 70 | application/json: 71 | schema: 72 | $ref: '#/components/schemas/ValidatorRequest' 73 | required: true 74 | responses: 75 | '200': 76 | description: Validation summary 77 | content: 78 | application/json: 79 | schema: 80 | $ref: '#/components/schemas/ValidationSummary' 81 | '500': 82 | description: Error 83 | content: 84 | application/json: 85 | schema: 86 | $ref: '#/components/schemas/Error' 87 | components: 88 | schemas: 89 | Error: 90 | type: object 91 | Validator: 92 | required: 93 | - summary 94 | - files 95 | type: object 96 | properties: 97 | summary: 98 | type: object 99 | files: 100 | type: array 101 | items: 102 | $ref: '#/components/schemas/ValidatedFile' 103 | ValidatedFile: 104 | type: object 105 | properties: 106 | schema: 107 | type: object 108 | errors: 109 | type: array 110 | items: 111 | $ref: '#/components/schemas/JSONError' 112 | url: 113 | type: string 114 | version: 115 | type: string 116 | recommanded: 117 | type: boolean 118 | required: 119 | type: boolean 120 | exists: 121 | type: boolean 122 | file: 123 | type: string 124 | hasErrors: 125 | type: boolean 126 | errorsCount: 127 | type: number 128 | required: 129 | - required 130 | - exists 131 | - file 132 | - hasErrors 133 | - errorsCount 134 | JSONError: 135 | type: object 136 | properties: 137 | instancePath: 138 | type: string 139 | schemaPath: 140 | type: string 141 | keyword: 142 | type: string 143 | params: 144 | type: object 145 | message: 146 | type: string 147 | ValidatorRequest: 148 | type: object 149 | properties: 150 | url: 151 | type: string 152 | options: 153 | type: object 154 | properties: 155 | freefloating: 156 | type: boolean 157 | docked: 158 | type: boolean 159 | version: 160 | type: string 161 | auth: 162 | type: object 163 | properties: 164 | type: 165 | type: string 166 | basicAuth: 167 | type: object 168 | properties: 169 | user: 170 | type: string 171 | password: 172 | type: string 173 | bearerToken: 174 | type: object 175 | properties: 176 | token: 177 | type: string 178 | oauthClientCredentialsGrant: 179 | type: object 180 | properties: 181 | user: 182 | type: string 183 | password: 184 | type: string 185 | tokenUrl: 186 | type: string 187 | required: 188 | - url 189 | FeedRequest: 190 | required: 191 | - url 192 | type: object 193 | properties: 194 | url: 195 | type: string 196 | Feed: 197 | type: object 198 | properties: 199 | summary: 200 | type: object 201 | gbfsResult: 202 | type: object 203 | gbfsVersion: 204 | type: string 205 | files: 206 | type: array 207 | items: 208 | type: object 209 | ValidationSummary: 210 | type: object 211 | properties: 212 | summary: 213 | type: object 214 | filesSummary: 215 | type: array 216 | items: 217 | type: object 218 | properties: 219 | required: 220 | type: boolean 221 | exists: 222 | type: boolean 223 | file: 224 | type: string 225 | hasErrors: 226 | type: boolean 227 | errorsCount: 228 | type: number 229 | groupedErrors: 230 | type: array 231 | items: 232 | type: object 233 | properties: 234 | keyword: 235 | type: string 236 | message: 237 | type: string 238 | schemaPath: 239 | type: string 240 | count: 241 | type: number 242 | required: 243 | - required 244 | - exists 245 | - file 246 | - hasErrors 247 | - errorsCount 248 | -------------------------------------------------------------------------------- /docs/local-test.md: -------------------------------------------------------------------------------- 1 | # How to test GBFS Validator locally 2 | 3 | ## 1. Download each file of a feed 4 | 5 | For example, use this [Bolt feed](https://mds.bolt.eu/gbfs/3/336/gbfs) that contains the 3 major versions. Put the downloaded file in an empty folder, like `gbfs`. 6 | 7 | ## 2. Introduce errors in the local feed 8 | 9 | Introduce errors as per requirement. 10 | 11 | ## 3. Run Commands in `gbfs-validator` project folder 12 | 13 | 1. Navigate to the `gbfs-validator` folder: 14 | ```sh 15 | cd gbfs-validator 16 | ``` 17 | 18 | 2. Install dependencies: 19 | ```sh 20 | yarn 21 | ``` 22 | 23 | 3. Start the development server: 24 | ```sh 25 | yarn run dev 26 | ``` 27 | 28 | If successful, you’ll see **Server now ready on http://localhost:8888** 29 | 30 | ![image.png](./local-test1.png) 31 | 32 | ## 4. Run a server locally in the folder that contains the local feed 33 | 34 | 1. Start a local server: 35 | ```sh 36 | python3 -m http.server 800 37 | ``` 38 | 39 | ![image.png](./local-test2.png) 40 | 41 | 2. Replace the feed URLs in the local gbfs.json file with the local URLs. 42 | 43 | Replace `https://mds.bolt.eu/gbfs/3/336/gbfs` with `http://localhost:800/gbfs/3/336/gbfs`. 44 | 45 | 3. Validate the feed 46 | 47 | Go to [http://localhost:8888](http://localhost:8888) and validate your local feed `http://localhost:800/gbfs/3/336/gbfs`. 48 | -------------------------------------------------------------------------------- /docs/local-test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/docs/local-test1.png -------------------------------------------------------------------------------- /docs/local-test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/docs/local-test2.png -------------------------------------------------------------------------------- /docs/update-json-schema.md: -------------------------------------------------------------------------------- 1 | To update the JSON schema to the latest version, follow these steps: 2 | 1. Open the gbfs-validator/package.json file: Locate the package.json file in your project directory. 3 | 2. Bump up the version number: Find the "version" field and update its value to the latest version. For example, if the latest version is 1.0.14, change it as follows: 4 | "version": "1.0.14", 5 | 3. Commit the changes: Save the file and commit the changes to your version control system (e.g., Git). 6 | 4. Create a PR: Create a PR: Create a pull request and validate the changes using the deploy preview link. 7 | -------------------------------------------------------------------------------- /functions/feed.js: -------------------------------------------------------------------------------- 1 | const GBFS = require('gbfs-validator'); 2 | const { defaultApplicationResponseHeaders, getCorsResponse } = require('../common/http-utils'); 3 | 4 | exports.handler = function (event, context, callback) { 5 | const corsResponse = getCorsResponse(event); 6 | if (corsResponse !== undefined) { 7 | callback(null, corsResponse) 8 | return; 9 | } 10 | 11 | let body 12 | 13 | try { 14 | body = JSON.parse(event.body) 15 | } catch (err) { 16 | callback(err, { 17 | headers: defaultApplicationResponseHeaders, 18 | statusCode: 500, 19 | body: JSON.stringify(err) 20 | }) 21 | } 22 | 23 | const gbfs = new GBFS(body.url) 24 | 25 | gbfs 26 | .getFiles() 27 | .then((result) => { 28 | callback(null, { 29 | headers: defaultApplicationResponseHeaders, 30 | statusCode: 200, 31 | body: JSON.stringify(result) 32 | }) 33 | }) 34 | .catch((err) => { 35 | callback(null, { 36 | headers: defaultApplicationResponseHeaders, 37 | statusCode: 500, 38 | body: JSON.stringify(err.message) 39 | }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "version": "1.0.0", 4 | "description": "validator-functions", 5 | "private": true, 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 0" 8 | }, 9 | "dependencies": { 10 | "gbfs-validator": "^1.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /functions/validator-summary.js: -------------------------------------------------------------------------------- 1 | const GBFS = require('gbfs-validator') 2 | 3 | /** @typedef {{ 4 | * summary: { 5 | * validatorVersion: string, 6 | * hasErrors: boolean, 7 | * errorsCount: number, 8 | * version: { 9 | * detected: string, 10 | * validated: string 11 | * } 12 | * filesSummary: [ 13 | * { 14 | * required: boolean, 15 | * exists: boolean, 16 | * hasErrors: boolean, 17 | * file: string, 18 | * errorsCount: number, 19 | * groupedErrors: [ 20 | * { 21 | * keyword: string, 22 | * message: string, 23 | * schemaPath: string, 24 | * count: number 25 | * } 26 | * ] 27 | * } 28 | * ] 29 | * } 30 | * }} Summary 31 | */ 32 | 33 | /** 34 | * This function returns a summary from the validator's response, stripping out the notices and grouping errors by message, keyword, and schemaPath. 35 | * 36 | * @param validationResult from the GBFS validator class 37 | * @returns { Summary } 38 | */ 39 | const getSummary = (validationResult) => ( 40 | { 41 | ...validationResult, 42 | files: undefined, 43 | filesSummary: (validationResult.files || []).map(item => ({ 44 | required: item.required, 45 | exists: item.exists, 46 | file: item.file, 47 | hasErrors: item.hasErrors, 48 | errorsCount: item.errorsCount, 49 | groupedErrors: item.exists && item.languages && item.languages[0] && item.languages[0].errors 50 | ? groupErrors(item.languages[0].errors) 51 | : [] 52 | })) 53 | } 54 | ) 55 | 56 | /** 57 | * Groups errors by keyword, message, and schemaPath, adding a count for each group. 58 | * 59 | * @param errors array of error objects 60 | * @returns {Array} grouped errors with count 61 | */ 62 | const groupErrors = (errors) => { 63 | const errorMap = {}; 64 | 65 | errors.forEach(error => { 66 | const key = `${error.keyword}-${error.message}-${error.schemaPath}`; 67 | if (errorMap[key]) { 68 | errorMap[key].count += 1; 69 | } else { 70 | errorMap[key] = { 71 | keyword: error.keyword, 72 | message: error.message, 73 | schemaPath: error.schemaPath, 74 | count: 1 75 | }; 76 | } 77 | }); 78 | 79 | return Object.values(errorMap); 80 | }; 81 | 82 | 83 | /** 84 | * call the callback function with {@link Summary} 85 | */ 86 | exports.handler = function (event, context, callback) { 87 | let body 88 | 89 | try { 90 | body = JSON.parse(event.body) 91 | } catch (err) { 92 | callback(err, { 93 | statusCode: 500, 94 | body: JSON.stringify(err) 95 | }) 96 | } 97 | 98 | const gbfs = new GBFS(body.url, body.options) 99 | 100 | gbfs 101 | .validation() 102 | .then(result => { 103 | callback(null, { 104 | statusCode: 200, 105 | body: JSON.stringify(getSummary(result)) 106 | }) 107 | }) 108 | .catch(err => { 109 | callback(null, { 110 | statusCode: 500, 111 | body: JSON.stringify(err.message) 112 | }) 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /functions/validator.js: -------------------------------------------------------------------------------- 1 | const GBFS = require('gbfs-validator'); 2 | const { defaultApplicationResponseHeaders, getCorsResponse } = require('../common/http-utils'); 3 | 4 | exports.handler = function (event, context, callback) { 5 | const corsResponse = getCorsResponse(event); 6 | if (corsResponse !== undefined) { 7 | callback(null, corsResponse) 8 | return; 9 | } 10 | 11 | let body 12 | 13 | try { 14 | body = JSON.parse(event.body) 15 | } catch (err) { 16 | callback(err, { 17 | headers: defaultApplicationResponseHeaders, 18 | statusCode: 500, 19 | body: JSON.stringify(err) 20 | }) 21 | } 22 | 23 | const gbfs = new GBFS(body.url, body.options) 24 | 25 | gbfs 26 | .validation() 27 | .then((result) => { 28 | callback(null, { 29 | headers: defaultApplicationResponseHeaders, 30 | statusCode: 200, 31 | body: JSON.stringify(result) 32 | }) 33 | }) 34 | .catch((err) => { 35 | callback(null, { 36 | headers: defaultApplicationResponseHeaders, 37 | statusCode: 500, 38 | body: JSON.stringify(err.message) 39 | }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /gbfs-validator/README.md: -------------------------------------------------------------------------------- 1 | # GBFS-Validator 2 | 3 | [![npm version](https://badge.fury.io/js/gbfs-validator.svg)](http://badge.fury.io/js/gbfs-validator) 4 | 5 | GBFS Validator is a command-line tool for validating General Bikeshare Feed Specification (GBFS) data feed and outputing the report in a JSON file 6 | 7 | ## Add the Dependency 8 | 9 | To use `gbfs-validator` in your own project, you need to 10 | first install our [Node.js npm package](https://www.npmjs.com/package/gbfs-validator): 11 | 12 | ``` 13 | npm install gbfs-validator 14 | ``` 15 | 16 | ## Supported GBFS Versions 17 | - 3.1-RC 18 | - 3.0 19 | - 2.3 20 | - 2.2 21 | - 2.1 22 | - 2.0 23 | - 1.1 24 | - 1.0 25 | 26 | ## Example Code 27 | ```javascript 28 | const GBFS = require('gbfs-validator'); 29 | 30 | const feedUrl = "https://gbfs.velobixi.com/gbfs/gbfs.json"; 31 | const feedOptions = {} 32 | const gbfs = new GBFS(feedUrl, feedOptions) 33 | 34 | gbfs.validation().then((reportResults) => { 35 | // reportResults: GBFS Validation Report Results in JSON 36 | }).catch(error => { 37 | // error handling 38 | }) 39 | 40 | gbfs.getFiles().then((gbfsFeedFiles) => { 41 | // gbfsFeedFiles: Info about GBFS feed 42 | }).catch(error => { 43 | // error handling 44 | }) 45 | ``` 46 | 47 | ## Usage of the Command Line Interface 48 | 49 | How to validate a feed and place the report in a located file 50 | ``` 51 | gbfs-validator -u {http_address_of_gbfs_dataset} -s {local_path_to_output_report_file} 52 | ``` 53 | 54 | ## Example of the CLI 55 | 56 | ``` 57 | gbfs-validator -u https://gbfs.velobixi.com/gbfs/gbfs.json -s ~/Documents/log.json 58 | ``` 59 | 60 | ## Options 61 | 62 | ``` 63 | -v, --version: output the version number 64 | -u, --url : URL of the GBFS feed 65 | -vb, --verbose: Verbose mode prints debugging console logs 66 | -s, --save-report : Local path to output report file 67 | -pr, --print-report : Print report to standard output (choices: "yes", "no", default: "yes") 68 | -h, --help: display help for command 69 | ``` -------------------------------------------------------------------------------- /gbfs-validator/__test__/__snapshots__/gbfs.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`initialization should correctly initialize validator per default 1`] = ` 4 | GBFS { 5 | "auth": Object {}, 6 | "gotOptions": Object { 7 | "headers": Object { 8 | "user-agent": "MobilityData GBFS-Validator/1.1.1 (Node 16.0.1)", 9 | }, 10 | }, 11 | "options": Object { 12 | "docked": false, 13 | "freefloating": false, 14 | "version": null, 15 | }, 16 | "url": "http://localhost:8888/gbfs.json", 17 | } 18 | `; 19 | 20 | exports[`initialization should correctly initialize validator with options 1`] = ` 21 | GBFS { 22 | "auth": Object {}, 23 | "gotOptions": Object { 24 | "headers": Object { 25 | "user-agent": "MobilityData GBFS-Validator/1.1.1 (Node 16.0.1)", 26 | }, 27 | }, 28 | "options": Object { 29 | "docked": true, 30 | "freefloating": true, 31 | "version": "v2.1", 32 | }, 33 | "url": "http://localhost:8888/gbfs.json", 34 | } 35 | `; 36 | 37 | exports[`initialization should throw an error without url 1`] = `"Missing URL"`; 38 | 39 | exports[`initialization with auth should correctly initialize with \`basic_auth\` 1`] = ` 40 | GBFS { 41 | "auth": Object { 42 | "basicAuth": Object { 43 | "password": "mypassword", 44 | "user": "myuser", 45 | }, 46 | "type": "basic_auth", 47 | }, 48 | "gotOptions": Object { 49 | "headers": Object { 50 | "Authorization": "basic bXl1c2VyOm15cGFzc3dvcmQ=", 51 | }, 52 | }, 53 | "options": Object { 54 | "docked": false, 55 | "freefloating": false, 56 | "version": null, 57 | }, 58 | "url": "http://localhost:8888/gbfs.json", 59 | } 60 | `; 61 | 62 | exports[`initialization with auth should correctly initialize with \`bearer_token\` 1`] = ` 63 | GBFS { 64 | "auth": Object { 65 | "bearerToken": Object { 66 | "token": "mytoken", 67 | }, 68 | "type": "bearer_token", 69 | }, 70 | "gotOptions": Object { 71 | "headers": Object { 72 | "Authorization": "Bearer mytoken", 73 | }, 74 | }, 75 | "options": Object { 76 | "docked": false, 77 | "freefloating": false, 78 | "version": null, 79 | }, 80 | "url": "http://localhost:8888/gbfs.json", 81 | } 82 | `; 83 | 84 | exports[`initialization with auth should correctly initialize with multiple \`headers\` 1`] = ` 85 | GBFS { 86 | "auth": Object { 87 | "headers": Array [ 88 | Object { 89 | "key": "mykey", 90 | "value": "myvalue", 91 | }, 92 | Object { 93 | "key": "mysecondkey", 94 | "value": "mysecondvalue", 95 | }, 96 | Object { 97 | "key": "mythirdkey", 98 | "value": "mythirdvalue", 99 | }, 100 | ], 101 | "type": "headers", 102 | }, 103 | "gotOptions": Object { 104 | "headers": Object { 105 | "mykey": "myvalue", 106 | "mysecondkey": "mysecondvalue", 107 | "mythirdkey": "mythirdvalue", 108 | "user-agent": "MobilityData GBFS-Validator/1.1.1 (Node 16.0.1)", 109 | }, 110 | }, 111 | "options": Object { 112 | "docked": false, 113 | "freefloating": false, 114 | "version": null, 115 | }, 116 | "url": "http://localhost:8888/gbfs.json", 117 | } 118 | `; 119 | 120 | exports[`initialization with auth should correctly initialize with one \`headers\` 1`] = ` 121 | GBFS { 122 | "auth": Object { 123 | "headers": Array [ 124 | Object { 125 | "key": "mykey", 126 | "value": "myvalue", 127 | }, 128 | ], 129 | "type": "headers", 130 | }, 131 | "gotOptions": Object { 132 | "headers": Object { 133 | "mykey": "myvalue", 134 | "user-agent": "MobilityData GBFS-Validator/1.1.1 (Node 16.0.1)", 135 | }, 136 | }, 137 | "options": Object { 138 | "docked": false, 139 | "freefloating": false, 140 | "version": null, 141 | }, 142 | "url": "http://localhost:8888/gbfs.json", 143 | } 144 | `; 145 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/cli.test.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | let exec = require('node:child_process').exec 3 | 4 | const serverOpts = { 5 | port: 0, 6 | host: '127.0.0.1', 7 | } 8 | 9 | const cliExec = (args) => { 10 | const command = `${path.resolve(`${__dirname}/../cli.js`)} ${args.join(' ')}` 11 | return new Promise(resolve => { 12 | exec(command, 13 | // Setting exit override to allow program to exit simplifying unit testing 14 | { env: { ...process.env, 'EXIT_OVERRIDE': 'true' } }, 15 | (error, stdout, stderr) => { 16 | resolve({ 17 | code: error && error.code ? error.code : 0, 18 | error, 19 | stdout, 20 | stderr 21 | }) 22 | }) 23 | }) 24 | } 25 | 26 | describe('cli', () => { 27 | 28 | let gbfsFeedServer 29 | let feedUrl 30 | 31 | beforeAll(async () => { 32 | gbfsFeedServer = require('./fixtures/server')() 33 | await gbfsFeedServer.listen(serverOpts) 34 | feedUrl = `http://${gbfsFeedServer.server.address().address}:${gbfsFeedServer.server.address().port}/gbfs.json` 35 | }) 36 | 37 | afterAll(() => { 38 | return gbfsFeedServer.close() 39 | }) 40 | 41 | test('should show an error if url parameter is not set', async () => { 42 | const result = await cliExec([]) 43 | expect(result.code).toBe(1) 44 | expect(result.error.message).toContain('error: required option \'-u, --url \' not specified') 45 | }) 46 | 47 | test('should success and print the report when url parameter set and -pr is set as default', async () => { 48 | const result = await cliExec([`-u`, `${feedUrl}`]) 49 | expect(result.code).toBe(0) 50 | expect(result.stdout).toContain('summary:') 51 | }) 52 | 53 | test('should show an error if pr parameter is set to `no` and -s is not set', async () => { 54 | const result = await cliExec([`-u ${feedUrl}`, '-pr no']) 55 | expect(result.code).toBe(1) 56 | expect(result.stdout).toContain('Please set at least one of the following options: --save-report or --print-report') 57 | }) 58 | 59 | test('should success when paramters url and save report has valid values and print report is set to no', async () => { 60 | const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-pr no']) 61 | expect(result.code).toBe(0) 62 | }) 63 | 64 | test('should success and print report when paramters url and save report are valid and print report is set to yes', async () => { 65 | const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-pr yes']) 66 | expect(result.code).toBe(0) 67 | expect(result.stdout).toContain('summary:') 68 | }) 69 | }) -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/conditional_default_reserve_time.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | function build(opts = {}) { 4 | const app = fastify(opts) 5 | 6 | app.get('/gbfs.json', async function(request, reply) { 7 | return { 8 | last_updated: "2024-05-23T15:30:00Z", 9 | ttl: 0, 10 | version: '3.1-RC', 11 | data: { 12 | feeds: [ 13 | { 14 | name: 'system_information', 15 | url: `http://${request.hostname}/system_information.json` 16 | }, 17 | { 18 | name: 'station_information', 19 | url: `http://${request.hostname}/station_information.json` 20 | }, 21 | { 22 | name: 'vehicle_types', 23 | url: `http://${request.hostname}/vehicle_types.json` 24 | }, 25 | { 26 | name: 'system_pricing_plans', 27 | url: `http://${request.hostname}/system_pricing_plans.json` 28 | }, 29 | ] 30 | 31 | } 32 | } 33 | }) 34 | 35 | app.get('/system_information.json', async function(request, reply) { 36 | return { 37 | last_updated: "2024-05-23T15:30:00Z", 38 | ttl: 0, 39 | version: '3.1-RC', 40 | data: { 41 | system_id: 'shared_bike', 42 | name: [ 43 | { 44 | text: 'Shared Bike USA', 45 | language: 'en' 46 | } 47 | ], 48 | timezone: 'Etc/UTC', 49 | opening_hours: "Mo-Fr 08:00-17:00", 50 | feed_contact_email: "gg@gmail.com", 51 | languages: ["en"] 52 | } 53 | } 54 | }) 55 | 56 | app.get('/vehicle_types.json', async function(request, reply) { 57 | return { 58 | last_updated: "2024-05-23T15:30:00Z", 59 | ttl: 0, 60 | version: '3.1-RC', 61 | data: { 62 | vehicle_types: [ 63 | { 64 | // default_reserve_time is required 65 | vehicle_type_id: 'abc123', 66 | form_factor: 'scooter', 67 | propulsion_type: 'human', 68 | name: [ 69 | { 70 | text: 'Example Bicycle', 71 | language: 'en' 72 | } 73 | ], 74 | //default_reserve_time: 30, // should throw error 75 | return_type: ['any_station', 'free_floating'], 76 | vehicle_assets: { 77 | icon_url: 'https://www.example.com/assets/icon_bicycle.svg', 78 | icon_url_dark: 79 | 'https://www.example.com/assets/icon_bicycle_dark.svg', 80 | icon_last_modified: '2021-06-15' 81 | }, 82 | default_pricing_plan_id: 'car_plan_2', 83 | pricing_plan_ids: ['car_plan_2', 'car_plan_1'] 84 | }, 85 | { 86 | // default_reserve_time is required 87 | vehicle_type_id: 'efg456', 88 | form_factor: 'car', 89 | propulsion_type: 'electric', 90 | name: [ 91 | { 92 | text: 'Example Electric Car', 93 | language: 'en' 94 | } 95 | ], 96 | default_reserve_time: 30, 97 | max_range_meters: 100, 98 | return_type: ['any_station', 'free_floating'], 99 | vehicle_assets: { 100 | icon_url: 'https://www.example.com/assets/icon_car.svg', 101 | icon_url_dark: 'https://www.example.com/assets/icon_car_dark.svg', 102 | icon_last_modified: '2021-06-15' 103 | }, 104 | default_pricing_plan_id: 'car_plan_1', 105 | pricing_plan_ids: ['car_plan_1', 'car_plan_2', 'car_plan_3'] 106 | }, 107 | { 108 | // default_reserve_time is NOT required 109 | vehicle_type_id: 'efg4567', 110 | form_factor: 'car', 111 | propulsion_type: 'electric', 112 | name: [ 113 | { 114 | text: 'Example Electric Car 2', 115 | language: 'en' 116 | } 117 | ], 118 | //default_reserve_time: 30, 119 | max_range_meters: 100, 120 | return_type: ['any_station', 'free_floating'], 121 | vehicle_assets: { 122 | icon_url: 'https://www.example.com/assets/icon_car.svg', 123 | icon_url_dark: 'https://www.example.com/assets/icon_car_dark.svg', 124 | icon_last_modified: '2021-06-15' 125 | }, 126 | default_pricing_plan_id: 'car_plan_2', 127 | pricing_plan_ids: ['car_plan_2'] 128 | } 129 | ] 130 | } 131 | } 132 | }) 133 | 134 | app.get('/system_pricing_plans.json', async function(request, reply) { 135 | return { 136 | last_updated: "2024-05-23T15:30:00Z", 137 | ttl: 0, 138 | version: '3.1-RC', 139 | data: { 140 | plans: [ 141 | { 142 | plan_id: 'car_plan_1', 143 | name: [ 144 | { 145 | text: 'Basic', 146 | language: 'en' 147 | } 148 | ], 149 | currency: 'USD', 150 | price: 0, 151 | is_taxable: false, 152 | description: [ 153 | { 154 | text: 'Basic plan', 155 | language: 'en' 156 | } 157 | ], 158 | reservation_price_per_min: 3 159 | }, 160 | { 161 | plan_id: 'car_plan_2', 162 | name: [ 163 | { 164 | text: 'Basic 2', 165 | language: 'en' 166 | } 167 | ], 168 | currency: 'USD', 169 | price: 0, 170 | is_taxable: false, 171 | description: [ 172 | { 173 | text: 'Basic plan', 174 | language: 'en' 175 | } 176 | ], 177 | }, 178 | { 179 | plan_id: 'car_plan_3', 180 | name: [ 181 | { 182 | text: 'Basic 3', 183 | language: 'en' 184 | } 185 | ], 186 | currency: 'USD', 187 | price: 0, 188 | is_taxable: false, 189 | description: [ 190 | { 191 | text: 'Basic plan', 192 | language: 'en' 193 | } 194 | ], 195 | reservation_price_flat_rate: 5 196 | }, 197 | { 198 | plan_id: 'car_plan_4', 199 | name: [ 200 | { 201 | text: 'Basic 4', 202 | language: 'en' 203 | } 204 | ], 205 | currency: 'USD', 206 | price: 0, 207 | is_taxable: false, 208 | description: [ 209 | { 210 | text: 'Basic plan', 211 | language: 'en' 212 | } 213 | ], 214 | reservation_price_flat_rate: 5, 215 | reservation_price_per_min: 3 // this should throw an error 216 | } 217 | ] 218 | } 219 | } 220 | }) 221 | 222 | return app 223 | } 224 | 225 | module.exports = build 226 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/conditionnal_no_vehicle_type_id.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | function build(opts = {}) { 4 | const app = fastify(opts) 5 | 6 | app.get('/gbfs.json', async function(request, reply) { 7 | return { 8 | last_updated: 1566224400, 9 | ttl: 0, 10 | version: '2.2', 11 | data: { 12 | en: { 13 | feeds: [ 14 | { 15 | name: 'system_information', 16 | url: `http://${request.hostname}/system_information.json` 17 | }, 18 | { 19 | name: 'station_information', 20 | url: `http://${request.hostname}/station_information.json` 21 | }, 22 | { 23 | name: 'station_status', 24 | url: `http://${request.hostname}/station_status.json` 25 | }, 26 | { 27 | name: 'free_bike_status', 28 | url: `http://${request.hostname}/free_bike_status.json` 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | }) 35 | 36 | app.get('/system_information.json', async function(request, reply) { 37 | return { 38 | last_updated: 1566224400, 39 | ttl: 0, 40 | version: '2.2', 41 | data: { 42 | system_id: 'shared_bike', 43 | language: 'en', 44 | name: 'Shared Bike USA', 45 | timezone: 'Etc/UTC' 46 | } 47 | } 48 | }) 49 | 50 | app.get('/free_bike_status.json', async function(request, reply) { 51 | return { 52 | last_updated: 1566224400, 53 | ttl: 0, 54 | version: '2.2', 55 | data: { 56 | bikes: [ 57 | { 58 | bike_id: 'bike1', 59 | last_reported: 1609866109, 60 | lat: 12.345678, 61 | lon: 56.789012, 62 | is_reserved: false, 63 | is_disabled: false 64 | }, 65 | { 66 | bike_id: 'bike2', 67 | last_reported: 1609866109, 68 | lat: 12.345678, 69 | lon: 56.789012, 70 | is_reserved: false, 71 | is_disabled: false, 72 | vehicle_type_id: 'abc123' 73 | } 74 | ] 75 | } 76 | } 77 | }) 78 | 79 | return app 80 | } 81 | 82 | module.exports = build 83 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/conditionnal_vehicle_type_id.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | function build(opts = {}) { 4 | const app = fastify(opts) 5 | 6 | app.get('/gbfs.json', async function(request, reply) { 7 | return { 8 | last_updated: 1566224400, 9 | ttl: 0, 10 | version: '2.2', 11 | data: { 12 | en: { 13 | feeds: [ 14 | { 15 | name: 'system_information', 16 | url: `http://${request.hostname}/system_information.json` 17 | }, 18 | { 19 | name: 'station_information', 20 | url: `http://${request.hostname}/station_information.json` 21 | }, 22 | { 23 | name: 'station_status', 24 | url: `http://${request.hostname}/station_status.json` 25 | }, 26 | { 27 | name: 'free_bike_status', 28 | url: `http://${request.hostname}/free_bike_status.json` 29 | }, 30 | { 31 | name: 'vehicle_types', 32 | url: `http://${request.hostname}/vehicle_types.json` 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | }) 39 | 40 | app.get('/system_information.json', async function(request, reply) { 41 | return { 42 | last_updated: 1566224400, 43 | ttl: 0, 44 | version: '2.2', 45 | data: { 46 | system_id: 'shared_bike', 47 | language: 'en', 48 | name: 'Shared Bike USA', 49 | timezone: 'Etc/UTC' 50 | } 51 | } 52 | }) 53 | 54 | app.get('/vehicle_types.json', async function(request, reply) { 55 | return { 56 | last_updated: 1566224400, 57 | ttl: 0, 58 | version: '2.2', 59 | data: { 60 | vehicle_types: [ 61 | { 62 | vehicle_type_id: 'abc123', 63 | form_factor: 'bicycle', 64 | propulsion_type: 'human', 65 | name: 'Example Basic Bike', 66 | default_reserve_time: 30, 67 | return_type: ['any_station', 'free_floating'], 68 | vehicle_assets: { 69 | icon_url: 'https://www.example.com/assets/icon_bicycle.svg', 70 | icon_url_dark: 71 | 'https://www.example.com/assets/icon_bicycle_dark.svg', 72 | icon_last_modified: '2021-06-15' 73 | }, 74 | default_pricing_plan_id: 'bike_plan_1', 75 | pricing_plan_ids: ['bike_plan_1', 'bike_plan_2', 'bike_plan_3'] 76 | }, 77 | { 78 | vehicle_type_id: 'efg456', 79 | form_factor: 'car', 80 | propulsion_type: 'electric', 81 | name: 'Example Electric Car', 82 | default_reserve_time: 30, 83 | max_range_meters: 100, 84 | return_type: ['any_station', 'free_floating'], 85 | vehicle_assets: { 86 | icon_url: 'https://www.example.com/assets/icon_car.svg', 87 | icon_url_dark: 'https://www.example.com/assets/icon_car_dark.svg', 88 | icon_last_modified: '2021-06-15' 89 | }, 90 | default_pricing_plan_id: 'car_plan_1', 91 | pricing_plan_ids: ['car_plan_1', 'car_plan_2', 'car_plan_3'] 92 | } 93 | ] 94 | } 95 | } 96 | }) 97 | 98 | app.get('/free_bike_status.json', async function(request, reply) { 99 | return { 100 | last_updated: 1566224400, 101 | ttl: 0, 102 | version: '2.2', 103 | data: { 104 | bikes: [ 105 | { 106 | bike_id: 'bike1', 107 | last_reported: 1609866109, 108 | lat: 12.345678, 109 | lon: 56.789012, 110 | is_reserved: false, 111 | is_disabled: false 112 | // missing vehicle_type_id 113 | }, 114 | { 115 | bike_id: 'bike2', 116 | last_reported: 1609866109, 117 | lat: 12.345678, 118 | lon: 56.789012, 119 | is_reserved: false, 120 | is_disabled: false, 121 | vehicle_type_id: 'abc123' 122 | }, 123 | { 124 | bike_id: 'car1', 125 | last_reported: 1609866109, 126 | lat: 12.345678, 127 | lon: 56.789012, 128 | is_reserved: false, 129 | is_disabled: false, 130 | vehicle_type_id: 'efg456' 131 | // missing current_range_meters 132 | } 133 | ] 134 | } 135 | } 136 | }) 137 | 138 | return app 139 | } 140 | 141 | module.exports = build 142 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/conditionnal_vehicle_types_available.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | function build(opts = {}) { 4 | const app = fastify(opts) 5 | 6 | app.get('/gbfs.json', async function(request, reply) { 7 | return { 8 | last_updated: 1566224400, 9 | ttl: 0, 10 | version: '2.2', 11 | data: { 12 | en: { 13 | feeds: [ 14 | { 15 | name: 'system_information', 16 | url: `http://${request.hostname}/system_information.json` 17 | }, 18 | { 19 | name: 'station_information', 20 | url: `http://${request.hostname}/station_information.json` 21 | }, 22 | { 23 | name: 'station_status', 24 | url: `http://${request.hostname}/station_status.json` 25 | }, 26 | { 27 | name: 'vehicle_types', 28 | url: `http://${request.hostname}/vehicle_types.json` 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | }) 35 | 36 | app.get('/system_information.json', async function(request, reply) { 37 | return { 38 | last_updated: 1566224400, 39 | ttl: 0, 40 | version: '2.2', 41 | data: { 42 | system_id: 'shared_bike', 43 | language: 'en', 44 | name: 'Shared Bike USA', 45 | timezone: 'Etc/UTC' 46 | } 47 | } 48 | }) 49 | 50 | app.get('/vehicle_types.json', async function(request, reply) { 51 | return { 52 | last_updated: 1566224400, 53 | ttl: 0, 54 | version: '2.2', 55 | data: { 56 | vehicle_types: [ 57 | { 58 | vehicle_type_id: 'abc123', 59 | form_factor: 'bicycle', 60 | propulsion_type: 'human', 61 | name: 'Example Basic Bike', 62 | default_reserve_time: 30, 63 | return_type: ['any_station', 'free_floating'], 64 | vehicle_assets: { 65 | icon_url: 'https://www.example.com/assets/icon_bicycle.svg', 66 | icon_url_dark: 67 | 'https://www.example.com/assets/icon_bicycle_dark.svg', 68 | icon_last_modified: '2021-06-15' 69 | }, 70 | default_pricing_plan_id: 'bike_plan_1', 71 | pricing_plan_ids: ['bike_plan_1', 'bike_plan_2', 'bike_plan_3'] 72 | }, 73 | { 74 | vehicle_type_id: 'efg456', 75 | form_factor: 'car', 76 | propulsion_type: 'electric', 77 | name: 'Example Electric Car', 78 | default_reserve_time: 30, 79 | max_range_meters: 100, 80 | return_type: ['any_station', 'free_floating'], 81 | vehicle_assets: { 82 | icon_url: 'https://www.example.com/assets/icon_car.svg', 83 | icon_url_dark: 'https://www.example.com/assets/icon_car_dark.svg', 84 | icon_last_modified: '2021-06-15' 85 | }, 86 | default_pricing_plan_id: 'car_plan_1', 87 | pricing_plan_ids: ['car_plan_1', 'car_plan_2', 'car_plan_3'] 88 | } 89 | ] 90 | } 91 | } 92 | }) 93 | 94 | app.get('/station_status.json', async function(request, reply) { 95 | return { 96 | last_updated: 1566224400, 97 | ttl: 0, 98 | version: '2.2', 99 | data: { 100 | stations: [ 101 | { 102 | station_id: 'abc123', 103 | num_bikes_available: 0, 104 | is_installed: true, 105 | is_renting: true, 106 | is_returning: true, 107 | last_reported: 1566224400 108 | // missing vehicle_types_available 109 | }, 110 | { 111 | station_id: 'abc123', 112 | num_bikes_available: 0, 113 | is_installed: true, 114 | is_renting: true, 115 | is_returning: true, 116 | last_reported: 1566224400, 117 | vehicle_types_available: [ 118 | { 119 | vehicle_type_id: 'efg456', 120 | count: 2 121 | } 122 | ] 123 | } 124 | ] 125 | } 126 | } 127 | }) 128 | 129 | return app 130 | } 131 | 132 | module.exports = build 133 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/missing_vehicle_types.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | function build(opts = {}) { 4 | const app = fastify(opts) 5 | 6 | app.get('/gbfs.json', async function(request, reply) { 7 | return { 8 | last_updated: 1566224400, 9 | ttl: 0, 10 | version: '2.2', 11 | data: { 12 | en: { 13 | feeds: [ 14 | { 15 | name: 'system_information', 16 | url: `http://${request.hostname}/system_information.json` 17 | }, 18 | { 19 | name: 'station_information', 20 | url: `http://${request.hostname}/station_information.json` 21 | }, 22 | { 23 | name: 'station_status', 24 | url: `http://${request.hostname}/station_status.json` 25 | }, 26 | { 27 | name: 'free_bike_status', 28 | url: `http://${request.hostname}/free_bike_status.json` 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | }) 35 | 36 | app.get('/system_information.json', async function(request, reply) { 37 | return { 38 | last_updated: 1566224400, 39 | ttl: 0, 40 | version: '2.2', 41 | data: { 42 | system_id: 'shared_bike', 43 | language: 'en', 44 | name: 'Shared Bike USA', 45 | timezone: 'Etc/UTC' 46 | } 47 | } 48 | }) 49 | 50 | app.get('/free_bike_status.json', async function(request, reply) { 51 | return { 52 | last_updated: 1566224400, 53 | ttl: 0, 54 | version: '2.2', 55 | data: { 56 | bikes: [ 57 | { 58 | bike_id: 'ghi789', 59 | last_reported: 1609866109, 60 | lat: 12.345678, 61 | lon: 56.789012, 62 | is_reserved: false, 63 | is_disabled: false, 64 | vehicle_type_id: 'abc123' 65 | } 66 | ] 67 | } 68 | } 69 | }) 70 | 71 | return app 72 | } 73 | 74 | module.exports = build 75 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/plan_id.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | function build(opts = {}) { 4 | const app = fastify(opts) 5 | 6 | app.get('/gbfs.json', async function(request, reply) { 7 | return { 8 | last_updated: 1566224400, 9 | ttl: 0, 10 | version: '2.3', 11 | data: { 12 | en: { 13 | feeds: [ 14 | { 15 | name: 'system_information', 16 | url: `http://${request.hostname}/system_information.json` 17 | }, 18 | { 19 | name: 'station_information', 20 | url: `http://${request.hostname}/station_information.json` 21 | }, 22 | { 23 | name: 'station_status', 24 | url: `http://${request.hostname}/station_status.json` 25 | }, 26 | { 27 | name: 'free_bike_status', 28 | url: `http://${request.hostname}/free_bike_status.json` 29 | }, 30 | { 31 | name: 'system_pricing_plans', 32 | url: `http://${request.hostname}/system_pricing_plans.json` 33 | }, 34 | { 35 | name: 'vehicle_types', 36 | url: `http://${request.hostname}/vehicle_types.json` 37 | } 38 | ] 39 | } 40 | } 41 | } 42 | }) 43 | 44 | app.get('/system_information.json', async function(request, reply) { 45 | return { 46 | last_updated: 1566224400, 47 | ttl: 0, 48 | version: '2.3', 49 | data: { 50 | system_id: 'shared_bike', 51 | language: 'en', 52 | name: 'Shared Bike USA', 53 | timezone: 'Etc/UTC' 54 | } 55 | } 56 | }) 57 | 58 | app.get('/free_bike_status.json', async function(request, reply) { 59 | return { 60 | last_updated: 1566224400, 61 | ttl: 0, 62 | version: '2.3', 63 | data: { 64 | bikes: [ 65 | { 66 | bike_id: 'ghi789', 67 | last_reported: 1609866109, 68 | lat: 12.345678, 69 | lon: 56.789012, 70 | is_reserved: false, 71 | is_disabled: false, 72 | vehicle_type_id: 'abc123' 73 | } 74 | ] 75 | } 76 | } 77 | }) 78 | 79 | app.get('/system_pricing_plans.json', async function(request, reply) { 80 | return { 81 | last_updated: 1566224400, 82 | ttl: 0, 83 | version: '2.3', 84 | data: { 85 | plans: [ 86 | { 87 | plan_id: 'p1', 88 | name: 'Basic', 89 | currency: 'USD', 90 | price: 0, 91 | is_taxable: false, 92 | description: 'Basic plan' 93 | } 94 | ] 95 | } 96 | } 97 | }) 98 | 99 | app.get('/vehicle_types.json', async function(request, reply) { 100 | return { 101 | last_updated: 1566224400, 102 | ttl: 0, 103 | version: '2.3', 104 | data: { 105 | vehicle_types: [ 106 | { 107 | vehicle_type_id: 'abc123', 108 | form_factor: 'bicycle', 109 | propulsion_type: 'human', 110 | name: 'Example Basic Bike', 111 | default_reserve_time: 30, 112 | return_type: ['any_station', 'free_floating'], 113 | vehicle_assets: { 114 | icon_url: 'https://www.example.com/assets/icon_bicycle.svg', 115 | icon_url_dark: 116 | 'https://www.example.com/assets/icon_bicycle_dark.svg', 117 | icon_last_modified: '2021-06-15' 118 | }, 119 | default_pricing_plan_id: 'bike_plan_1', 120 | pricing_plan_ids: ['bike_plan_1', 'bike_plan_2', 'bike_plan_3'] 121 | }, 122 | { 123 | vehicle_type_id: 'efg456', 124 | form_factor: 'car', 125 | propulsion_type: 'electric', 126 | name: 'Example Electric Car', 127 | default_reserve_time: 30, 128 | max_range_meters: 100, 129 | return_type: ['any_station', 'free_floating'], 130 | vehicle_assets: { 131 | icon_url: 'https://www.example.com/assets/icon_car.svg', 132 | icon_url_dark: 'https://www.example.com/assets/icon_car_dark.svg', 133 | icon_last_modified: '2021-06-15' 134 | }, 135 | //default_pricing_plan_id: 'car_plan_1', 136 | pricing_plan_ids: ['car_plan_1', 'car_plan_2', 'car_plan_3'] 137 | } 138 | ] 139 | } 140 | } 141 | }) 142 | 143 | return app 144 | } 145 | 146 | module.exports = build 147 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/server.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | function build(opts = {}) { 4 | const app = fastify(opts) 5 | 6 | app.get('/gbfs.json', async function(request, reply) { 7 | return { 8 | last_updated: 1566224400, 9 | ttl: 0, 10 | version: '2.2', 11 | data: { 12 | en: { 13 | feeds: [ 14 | { 15 | name: 'system_information', 16 | url: `http://${request.hostname}/system_information.json` 17 | }, 18 | { 19 | name: 'station_information', 20 | url: `http://${request.hostname}/station_information.json` 21 | }, 22 | { 23 | name: 'station_status', 24 | url: `http://${request.hostname}/station_status.json` 25 | }, 26 | { 27 | name: 'free_bike_status', 28 | url: `http://${request.hostname}/free_bike_status.json` 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | }) 35 | 36 | app.get('/autodiscovery/system_information.json', async function( 37 | request, 38 | reply 39 | ) { 40 | return { 41 | last_updated: 1566224400, 42 | ttl: 0, 43 | version: '2.2', 44 | data: { 45 | system_id: 'shared_bike', 46 | language: 'en', 47 | name: 'Shared Bike USA', 48 | timezone: 'Etc/UTC' 49 | } 50 | } 51 | }) 52 | 53 | app.get('/system_information.json', async function(request, reply) { 54 | return { 55 | last_updated: 1566224400, 56 | ttl: 0, 57 | version: '2.2', 58 | data: { 59 | system_id: 'shared_bike', 60 | // language: 'en', // Missing language 61 | name: 'Shared Bike USA', 62 | timezone: 'Etc/UTC' 63 | } 64 | } 65 | }) 66 | 67 | return app 68 | } 69 | 70 | module.exports = build 71 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/fixtures/v3.0/default.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify') 2 | 3 | const version = '3.0' 4 | const last_updated = '2019-08-19T10:20:00-04:00' 5 | const last_updated_fresh = new Date().toISOString() 6 | 7 | class MockRequests { 8 | entry_points() { 9 | return { 10 | manifest: this.manifest, 11 | gbfs: this.gbfs, 12 | gbfs_versions: this.gbfs_versions, 13 | system_information: this.system_information, 14 | vehicle_types: this.vehicle_types, 15 | station_status: this.station_status, 16 | vehicle_status: this.vehicle_status 17 | } 18 | } 19 | 20 | manifest({ basePath }) { 21 | return { 22 | last_updated, 23 | ttl: 0, 24 | version, 25 | data: { 26 | datasets: [ 27 | { 28 | system_id: 'example_berlin', 29 | versions: [ 30 | { 31 | version: '3.0', 32 | url: `${basePath}/gbfs.json` 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | } 39 | } 40 | 41 | gbfs({ basePath }) { 42 | return { 43 | last_updated, 44 | ttl: 0, 45 | version, 46 | data: { 47 | feeds: [ 48 | { 49 | name: 'system_information', 50 | url: `${basePath}/system_information.json` 51 | }, 52 | { 53 | name: 'vehicle_types', 54 | url: `${basePath}/vehicle_types.json` 55 | }, 56 | { 57 | name: 'vehicle_status', 58 | url: `${basePath}/vehicle_status.json` 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | 65 | gbfs_versions({ basePath }) { 66 | return { 67 | last_updated, 68 | ttl: 0, 69 | version, 70 | data: { 71 | versions: [ 72 | { 73 | version: '3.0', 74 | url: `${basePath}/gbfs.json` 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | 81 | system_information() { 82 | return { 83 | last_updated, 84 | ttl: 0, 85 | version, 86 | data: { 87 | system_id: 'shared_bike', 88 | languages: ['en'], 89 | name: [ 90 | { 91 | text: 'Shared Bike USA', 92 | language: 'en' 93 | } 94 | ], 95 | timezone: 'Etc/UTC', 96 | opening_hours: 'Mo-Su 00:00-23:59', 97 | feed_contact_email: 'datafeed@example.com' 98 | } 99 | } 100 | } 101 | 102 | vehicle_types() { 103 | return { 104 | last_updated, 105 | ttl: 0, 106 | version, 107 | data: { 108 | vehicle_types: [ 109 | { 110 | vehicle_type_id: 'biketype1', 111 | form_factor: 'bicycle', 112 | propulsion_type: 'human', 113 | name: [ 114 | { 115 | text: 'Example Basic Bike', 116 | language: 'en' 117 | } 118 | ], 119 | default_reserve_time: 30, 120 | return_type: ['any_station', 'free_floating'], 121 | vehicle_assets: { 122 | icon_url: 'https://www.example.com/assets/icon_bicycle.svg', 123 | icon_url_dark: 124 | 'https://www.example.com/assets/icon_bicycle_dark.svg', 125 | icon_last_modified: '2021-06-15' 126 | }, 127 | default_pricing_plan_id: 'bike_plan_1', 128 | pricing_plan_ids: ['bike_plan_1', 'bike_plan_2', 'bike_plan_3'] 129 | }, 130 | { 131 | vehicle_type_id: 'cartype1', 132 | form_factor: 'car', 133 | propulsion_type: 'electric', 134 | name: [ 135 | { 136 | text: 'Example Electric Car', 137 | language: 'en' 138 | } 139 | ], 140 | default_reserve_time: 30, 141 | max_range_meters: 100, 142 | return_type: ['any_station', 'free_floating'], 143 | vehicle_assets: { 144 | icon_url: 'https://www.example.com/assets/icon_car.svg', 145 | icon_url_dark: 'https://www.example.com/assets/icon_car_dark.svg', 146 | icon_last_modified: '2021-06-15' 147 | }, 148 | default_pricing_plan_id: 'car_plan_1', 149 | pricing_plan_ids: ['car_plan_1', 'car_plan_2', 'car_plan_3'] 150 | } 151 | ] 152 | } 153 | } 154 | } 155 | 156 | vehicle_status() { 157 | return { 158 | last_updated: last_updated_fresh, 159 | ttl: 0, 160 | version, 161 | data: { 162 | vehicles: [ 163 | { 164 | vehicle_id: 'bike1', 165 | last_reported: last_updated, 166 | lat: 12.345678, 167 | lon: 56.789012, 168 | is_reserved: false, 169 | is_disabled: false, 170 | vehicle_type_id: 'biketype1' 171 | }, 172 | { 173 | vehicle_id: 'car1', 174 | last_reported: last_updated, 175 | lat: 12.345678, 176 | lon: 56.789012, 177 | is_reserved: false, 178 | is_disabled: false, 179 | vehicle_type_id: 'cartype1', 180 | current_range_meters: 10 181 | } 182 | ] 183 | } 184 | } 185 | } 186 | 187 | build() { 188 | const app = fastify() 189 | 190 | const data = this.entry_points() 191 | 192 | const keys = Object.keys(data) 193 | 194 | for (const key of keys) { 195 | app.get(`/${key}.json`, async function(request) { 196 | const basePath = `http://${request.hostname}` 197 | 198 | return data[key]({ request, basePath }) 199 | }) 200 | } 201 | 202 | return app 203 | } 204 | } 205 | 206 | module.exports = { 207 | MockRequests 208 | } 209 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/gbfs.v3.0.test.js: -------------------------------------------------------------------------------- 1 | const GBFS = require('../gbfs') 2 | 3 | const serverOpts = { 4 | port: 0, 5 | host: '127.0.0.1', 6 | } 7 | 8 | function get_errors(result) { 9 | let errors = [] 10 | 11 | result.files?.map(f => { 12 | if (f.errors) { 13 | errors.push({ file: f.file, errors: f.errors }) 14 | } 15 | 16 | f.languages?.map(l => { 17 | if (l.errors) { 18 | errors.push({ file: f.file, lang: l.lang, errors: l.errors }) 19 | } 20 | }) 21 | }) 22 | 23 | return errors 24 | } 25 | 26 | describe('default feed', () => { 27 | let gbfsFeedServer 28 | 29 | beforeAll(async () => { 30 | const { MockRequests } = require('./fixtures/v3.0/default') 31 | let mockRequests = new MockRequests() 32 | 33 | gbfsFeedServer = mockRequests.build() 34 | 35 | await gbfsFeedServer.listen(serverOpts) 36 | }) 37 | 38 | afterAll(() => { 39 | return gbfsFeedServer.close() 40 | }) 41 | 42 | test('should validate feed', async () => { 43 | const url = `http://${gbfsFeedServer.server.address().address}:${ 44 | gbfsFeedServer.server.address().port 45 | }` 46 | const gbfs = new GBFS(`${url}/gbfs.json`) 47 | 48 | expect.assertions(1) 49 | 50 | return gbfs.validation().then(result => { 51 | expect(result).toMatchObject({ 52 | summary: expect.objectContaining({ 53 | version: { detected: '3.0', validated: '3.0' }, 54 | hasErrors: false 55 | }), 56 | files: expect.any(Array) 57 | }) 58 | }) 59 | }) 60 | }) 61 | 62 | describe('invalid feed', () => { 63 | let gbfsFeedServer 64 | 65 | beforeAll(async () => { 66 | const { MockRequests } = require('./fixtures/v3.0/default') 67 | class InvalidMockRequests extends MockRequests { 68 | system_information(...args) { 69 | const json = super.system_information(...args) 70 | 71 | delete json.data.name 72 | 73 | return json 74 | } 75 | } 76 | 77 | let mockRequests = new InvalidMockRequests() 78 | 79 | gbfsFeedServer = mockRequests.build() 80 | 81 | await gbfsFeedServer.listen(serverOpts) 82 | }) 83 | 84 | afterAll(() => { 85 | return gbfsFeedServer.close() 86 | }) 87 | 88 | test('should not validate feed', async () => { 89 | const url = `http://${gbfsFeedServer.server.address().address}:${ 90 | gbfsFeedServer.server.address().port 91 | }` 92 | const gbfs = new GBFS(`${url}/gbfs.json`) 93 | 94 | expect.assertions(2) 95 | 96 | return gbfs.validation().then(result => { 97 | expect(result).toMatchObject({ 98 | summary: expect.objectContaining({ 99 | version: { detected: '3.0', validated: '3.0' }, 100 | hasErrors: true, 101 | errorsCount: 1 102 | }), 103 | files: expect.any(Array) 104 | }) 105 | 106 | let error = result.files.find(f => f.file === 'system_information.json') 107 | ?.languages?.[0].errors?.[0].schemaPath 108 | expect(error).toBe('#/properties/data/required') 109 | }) 110 | }) 111 | }) 112 | 113 | describe('exaustive feed', () => { 114 | let gbfsFeedServer 115 | 116 | beforeAll(async () => { 117 | const { MockRequests } = require('./fixtures/v3.0/exaustive') 118 | 119 | let mockRequests = new MockRequests() 120 | 121 | gbfsFeedServer = mockRequests.build() 122 | 123 | await gbfsFeedServer.listen(serverOpts) 124 | }) 125 | 126 | afterAll(() => { 127 | return gbfsFeedServer.close() 128 | }) 129 | 130 | test('should validate feed', async () => { 131 | const url = `http://${gbfsFeedServer.server.address().address}:${ 132 | gbfsFeedServer.server.address().port 133 | }` 134 | const gbfs = new GBFS(`${url}/gbfs.json`) 135 | 136 | expect.assertions(2) 137 | 138 | return gbfs.validation().then(result => { 139 | expect(get_errors(result)).toEqual([]) 140 | 141 | expect(result).toMatchObject({ 142 | summary: expect.objectContaining({ 143 | version: { detected: '3.0', validated: '3.0' }, 144 | hasErrors: false 145 | }), 146 | files: expect.any(Array) 147 | }) 148 | }) 149 | }) 150 | }) 151 | -------------------------------------------------------------------------------- /gbfs-validator/__test__/index.test.js: -------------------------------------------------------------------------------- 1 | const GBFS = require('../gbfs') 2 | 3 | test('should export GBFS class', () => { 4 | const gbfs = require('../index') 5 | 6 | expect(gbfs).toBe(GBFS) 7 | }) 8 | -------------------------------------------------------------------------------- /gbfs-validator/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { inspect } = require('util') 4 | const commander = require('commander') 5 | const fs = require('fs') 6 | const fsPath = require('path') 7 | const GBFS = require('./gbfs') 8 | const pjson = require('./package.json') 9 | 10 | /** 11 | * This async function returns an Object that contains the FileValidationResult of all the files from a GBFS feed. 12 | * @returns {Object} 13 | */ 14 | getFeedValidationReport = async (url) => { 15 | const gbfs = new GBFS(url) 16 | return gbfs.validation() 17 | } 18 | 19 | /** 20 | * This function returns true if exit override param is set to 'true' to allow program to exit simplifying unit testing. 21 | * @returns {boolean} 22 | */ 23 | const isExitOverrided = () => (process.env.EXIT_OVERRIDE === 'true') 24 | 25 | /** 26 | * This function returns true if print report option is set to 'true'. 27 | * @param {Object} options - The CLI commander options. 28 | * @returns {boolean} 29 | */ 30 | const printingReport = (options) => (options.printReport === 'yes') 31 | 32 | /** 33 | * This function exits the process 34 | * @param {number} code - The exit process code. 35 | */ 36 | const exitProcess = (code) => { 37 | if (!isExitOverrided && code === 1) { 38 | process.exit(code) 39 | } 40 | process.exit(0) 41 | } 42 | 43 | /** 44 | * This function defines the options that can be passed by the user to the CLI, 45 | * its version and the usage description in the first line of the help. 46 | */ 47 | parseOptions = () => { 48 | commander 49 | .version(pjson.version, '-v, --version') 50 | .usage('[OPTIONS]...') 51 | .requiredOption('-u, --url ', 'URL of the GBFS feed') 52 | .option('-vb, --verbose', 'Verbose mode prints debugging console logs') 53 | .option('-s, --save-report ', 'Local path to output report file') 54 | .addOption(new commander.Option('-pr, --print-report ', 'Print report to standard output').default('yes').choices(['yes', 'no'])) 55 | 56 | // Supporting friendly unit testing and possible CI integrations 57 | // The process throw an exception on parsing error in addition to the parsing error 58 | if (isExitOverrided()) { 59 | commander.exitOverride() 60 | } 61 | return commander.parse(process.argv).opts() 62 | } 63 | 64 | /** 65 | * This function writes the validation report to a local file. 66 | * @param {*} report - The validation report. 67 | * @param {*} filePath - The path to the local file to write. 68 | */ 69 | const saveReport = (report, filePath) => { 70 | const dirname = fsPath.dirname(filePath); 71 | if (!fs.existsSync(dirname)) { 72 | fs.mkdirSync(dirname, { recursive: true }); 73 | } 74 | fs.writeFileSync(filePath, JSON.stringify(report)) 75 | } 76 | 77 | /** 78 | * This asyn function validates the feed and saves the report if requested in the options 79 | * @param {*} options - The options passed by the user to the CLI. 80 | */ 81 | const processFeedValidation = async (options) => { 82 | if (options.verbose) { 83 | console.log("Started GBFS validation with options: \n " + inspect(options, { depth: null, colors: true })) 84 | } 85 | try { 86 | const report = await getFeedValidationReport(options.url) 87 | if (printingReport(options)) { 88 | console.log(inspect(report, { depth: null, colors: true })) 89 | } 90 | if (options.saveReport) { 91 | saveReport(report, options.saveReport) 92 | } 93 | } catch (error) { 94 | console.error(`Critical error while validating GBFS feed => ${error}`) 95 | exitProcess(1); 96 | } 97 | } 98 | 99 | /** 100 | * This function checks that the options passed by the user are valid and 101 | * calls the processFeedValidation() function. 102 | */ 103 | const validate = () => { 104 | const options = parseOptions() 105 | if (!options.saveReport && !printingReport(options)) { 106 | console.log('Please set at least one of the following options: --save-report or --print-report') 107 | commander.help() 108 | exitProcess(1) 109 | } 110 | 111 | processFeedValidation(options).then( 112 | () => { 113 | if (options.verbose) { 114 | console.log("Validation completed") 115 | } 116 | } 117 | ) 118 | } 119 | 120 | if (require.main === module) { 121 | validate() 122 | } else { 123 | module.exports = { 124 | validate, 125 | processFeedValidation, 126 | saveReport, 127 | getFeedValidationReport, 128 | } 129 | } -------------------------------------------------------------------------------- /gbfs-validator/index.js: -------------------------------------------------------------------------------- 1 | const GBFS = require('./gbfs') 2 | 3 | module.exports = GBFS 4 | -------------------------------------------------------------------------------- /gbfs-validator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gbfs-validator", 3 | "version": "1.0.13", 4 | "author": "MobilityData", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "description": "Node-based libraries to validate GBFS feeds", 8 | "tags": [ 9 | "gbfs-validator" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/MobilityData/gbfs-validator.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/MobilityData/gbfs-validator/issues" 17 | }, 18 | "bin": { 19 | "gbfs-validator": "./cli.js" 20 | }, 21 | "scripts": { 22 | "test": "jest", 23 | "prepare": "git submodule update --init --recursive" 24 | }, 25 | "dependencies": { 26 | "ajv": "^8.9.0", 27 | "ajv-errors": "^3.0.0", 28 | "ajv-formats": "^2.1.1", 29 | "commander": "^11.0.0", 30 | "fast-json-patch": "^3.1.0", 31 | "got": "^11.8.2", 32 | "json-merge-patch": "^1.0.2" 33 | }, 34 | "devDependencies": { 35 | "fastify": "^3.20.2", 36 | "jest": "^27.0.6" 37 | }, 38 | "jest": { 39 | "collectCoverage": true, 40 | "collectCoverageFrom": [ 41 | "**/*.js", 42 | "!**/node_modules/**", 43 | "!**/coverage/**", 44 | "!**/__test__/**", 45 | "!**/schema/**" 46 | ], 47 | "testPathIgnorePatterns": [ 48 | "versions/gbfs-json-schema/" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gbfs-validator/validate.js: -------------------------------------------------------------------------------- 1 | const Ajv = require('ajv') 2 | const addFormats = require('ajv-formats') 3 | const ajvErrors = require('ajv-errors') 4 | const jsonpatch = require('fast-json-patch') 5 | const jsonmerge = require('json-merge-patch') 6 | 7 | /** 8 | * This function validates a file from a GBFS feed using Ajv. 9 | * @param {*} schema - The JSON schema for the file to validate. 10 | * @param {*} object - The body of a file to validate from a GBFS feed. 11 | * @param {*} options - An Object that contains an array of JSON Patches. 12 | * @returns {Object} - An Object tthat contains the schema and the errors detected. 13 | */ 14 | module.exports = function validate(schema, object, options = {}) { 15 | const ajv = new Ajv({ allErrors: true, strict: false }) 16 | ajvErrors(ajv) 17 | addFormats(ajv) 18 | 19 | let document = JSON.parse(JSON.stringify(schema)) 20 | 21 | options.addSchema?.map(add => { 22 | if (add.$patch) { 23 | if (add.$patch.source.$ref !== document.$id) { 24 | throw new Error( 25 | `Source of patch (${ 26 | add.$patch.source.$ref 27 | }) is not the same as the document (${document.$id})` 28 | ) 29 | } 30 | 31 | document = jsonpatch.applyPatch(document, add.$patch.with).newDocument 32 | } 33 | 34 | if (add.$merge) { 35 | if (add.$merge.source.$ref !== document.$id) { 36 | throw new Error( 37 | `Source of merge (${ 38 | add.$merge.source.$ref 39 | }) is not the same as the document (${document.$id})` 40 | ) 41 | } 42 | 43 | document = jsonmerge.apply(document, add.$merge.with) 44 | } 45 | }) 46 | 47 | let validate = ajv.compile(document) 48 | 49 | const valid = validate(object) 50 | 51 | if (valid) { 52 | return { 53 | schema: document, 54 | errors: false 55 | } 56 | } else { 57 | return { 58 | schema: document, 59 | errors: validate.errors.filter( 60 | e => !['$patch', '$merge', 'if'].includes(e.keyword) 61 | ) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gbfs-validator/versions/README.md: -------------------------------------------------------------------------------- 1 | # GBFS versions 2 | 3 | This folder contains json schemas from `MobilityData/gbfs-json-schema` and additionnal configuration for the validator (one additionnal file per version) 4 | 5 | ## JSON schemas 6 | 7 | The `schemas` folder contains a [git submodule](https://www.atlassian.com/git/tutorials/git-submodule) of https://github.com/MobilityData/gbfs-json-schema. 8 | 9 | You can pull schema update by using the command `git submodule update --init --recursive` 10 | 11 | ## gbfs-json-schema 12 | 13 | This directory is a git submodule, making changes in this directory is not recommended 14 | 15 | If you would like to make changes to `gbfs-json-schema` it is recommended to make a pull request from the original repository [gbfs-json-schema](https://github.com/MobilityData/gbfs-json-schema) 16 | 17 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.1/free_bike_status/required_vehicle_type_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | const partial = { 3 | $id: 'required_vehicle_type_id.json#' 4 | } 5 | 6 | const motorVehicleTypes = vehicleTypes.filter(vt => 7 | ['electric_assist', 'electric', 'combustion'].includes(vt.propulsion_type) 8 | ) 9 | 10 | if (motorVehicleTypes.length) { 11 | partial.$merge = { 12 | source: { 13 | $ref: 14 | 'https://github.com/MobilityData/gbfs/blob/v2.1/gbfs.md#free_bike_statusjson' 15 | }, 16 | with: { 17 | properties: { 18 | data: { 19 | properties: { 20 | bikes: { 21 | items: { 22 | errorMessage: { 23 | required: { 24 | vehicle_type_id: 25 | "'vehicle_type_id' is required for this vehicle type" 26 | } 27 | }, 28 | if: { 29 | properties: { 30 | vehicle_type_id: { 31 | enum: motorVehicleTypes.map(vt => vt.vehicle_type_id) 32 | } 33 | }, 34 | // "required" so it only trigger "then" when "vehicle_type_id" is present. 35 | required: ['vehicle_type_id'] 36 | }, 37 | then: { 38 | required: ['current_range_meters'] 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | partial.$patch = { 50 | source: { 51 | $ref: 52 | 'https://github.com/MobilityData/gbfs/blob/v2.1/gbfs.md#free_bike_statusjson' 53 | }, 54 | with: [ 55 | { 56 | op: 'add', 57 | path: '/properties/data/properties/bikes/items/required/0', 58 | value: 'vehicle_type_id' 59 | } 60 | ] 61 | } 62 | 63 | return partial 64 | } 65 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.1/station_status/required_vehicle_types_available.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | return { 3 | $id: 'required_vehicle_types_available.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v2.1/gbfs.md#station_statusjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | stations: { 14 | items: { 15 | properties: { 16 | vehicle_types_available: { 17 | items: { 18 | properties: { 19 | vehicle_type_id: { 20 | enum: vehicleTypes.map(vt => vt.vehicle_type_id) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | $patch: { 34 | source: { 35 | $ref: 36 | 'https://github.com/MobilityData/gbfs/blob/v2.1/gbfs.md#station_statusjson' 37 | }, 38 | with: [ 39 | { 40 | op: 'add', 41 | path: '/properties/data/properties/stations/items/required/0', 42 | value: 'vehicle_types_available' 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.1/system_information/required_store_uri.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ android = false, ios = false }) => { 2 | const partial = { 3 | $id: 'required_ios_store_uri.json#', 4 | $patch: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v2.1/gbfs.md#system_informationjson' 8 | }, 9 | with: [ 10 | { 11 | op: 'add', 12 | path: '/properties/data/required/0', 13 | value: 'rental_apps' 14 | } 15 | ] 16 | }, 17 | $merge: { 18 | source: { 19 | $ref: 20 | 'https://github.com/MobilityData/gbfs/blob/v2.1/gbfs.md#system_informationjson' 21 | }, 22 | with: { 23 | properties: { 24 | data: { 25 | properties: { 26 | rental_apps: { 27 | required: [], 28 | properties: { 29 | ios: { 30 | required: [] 31 | }, 32 | android: { 33 | required: [] 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (ios) { 45 | partial.$merge.with.properties.data.properties.rental_apps.required.push('ios') 46 | partial.$merge.with.properties.data.properties.rental_apps.properties.ios.required.push( 47 | 'store_uri' 48 | ) 49 | } 50 | 51 | if (android) { 52 | partial.$merge.with.properties.data.properties.rental_apps.required.push( 53 | 'android' 54 | ) 55 | partial.$merge.with.properties.data.properties.rental_apps.properties.android.required.push( 56 | 'store_uri' 57 | ) 58 | } 59 | 60 | return partial 61 | } 62 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.2/free_bike_status/required_vehicle_type_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | const partial = { 3 | $id: 'required_vehicle_type_id.json#' 4 | } 5 | 6 | const motorVehicleTypes = vehicleTypes.filter(vt => 7 | ['electric_assist', 'electric', 'combustion'].includes(vt.propulsion_type) 8 | ) 9 | 10 | if (motorVehicleTypes.length) { 11 | partial.$merge = { 12 | source: { 13 | $ref: 14 | 'https://github.com/MobilityData/gbfs/blob/v2.2/gbfs.md#free_bike_statusjson' 15 | }, 16 | with: { 17 | properties: { 18 | data: { 19 | properties: { 20 | bikes: { 21 | items: { 22 | errorMessage: { 23 | required: { 24 | vehicle_type_id: 25 | "'vehicle_type_id' is required for this vehicle type" 26 | } 27 | }, 28 | if: { 29 | properties: { 30 | vehicle_type_id: { 31 | enum: motorVehicleTypes.map(vt => vt.vehicle_type_id) 32 | } 33 | }, 34 | // "required" so it only trigger "then" when "vehicle_type_id" is present. 35 | required: ['vehicle_type_id'] 36 | }, 37 | then: { 38 | required: ['current_range_meters'] 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | partial.$patch = { 50 | source: { 51 | $ref: 52 | 'https://github.com/MobilityData/gbfs/blob/v2.2/gbfs.md#free_bike_statusjson' 53 | }, 54 | with: [ 55 | { 56 | op: 'add', 57 | path: '/properties/data/properties/bikes/items/required/0', 58 | value: 'vehicle_type_id' 59 | } 60 | ] 61 | } 62 | 63 | return partial 64 | } 65 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.2/station_status/required_vehicle_types_available.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | return { 3 | $id: 'required_vehicle_types_available.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v2.2/gbfs.md#station_statusjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | stations: { 14 | items: { 15 | properties: { 16 | vehicle_types_available: { 17 | items: { 18 | properties: { 19 | vehicle_type_id: { 20 | enum: vehicleTypes.map(vt => vt.vehicle_type_id) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | $patch: { 34 | source: { 35 | $ref: 36 | 'https://github.com/MobilityData/gbfs/blob/v2.2/gbfs.md#station_statusjson' 37 | }, 38 | with: [ 39 | { 40 | op: 'add', 41 | path: '/properties/data/properties/stations/items/required/0', 42 | value: 'vehicle_types_available' 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.2/system_information/required_store_uri.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ android = false, ios = false }) => { 2 | const partial = { 3 | $id: 'required_ios_store_uri.json#', 4 | $patch: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v2.2/gbfs.md#system_informationjson' 8 | }, 9 | with: [ 10 | { 11 | op: 'add', 12 | path: '/properties/data/required/0', 13 | value: 'rental_apps' 14 | } 15 | ] 16 | }, 17 | $merge: { 18 | source: { 19 | $ref: 20 | 'https://github.com/MobilityData/gbfs/blob/v2.2/gbfs.md#system_informationjson' 21 | }, 22 | with: { 23 | properties: { 24 | data: { 25 | properties: { 26 | rental_apps: { 27 | required: [], 28 | properties: { 29 | ios: { 30 | required: [] 31 | }, 32 | android: { 33 | required: [] 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (ios) { 45 | partial.$merge.with.properties.data.properties.rental_apps.required.push('ios') 46 | partial.$merge.with.properties.data.properties.rental_apps.properties.ios.required.push( 47 | 'store_uri' 48 | ) 49 | } 50 | 51 | if (android) { 52 | partial.$merge.with.properties.data.properties.rental_apps.required.push( 53 | 'android' 54 | ) 55 | partial.$merge.with.properties.data.properties.rental_apps.properties.android.required.push( 56 | 'store_uri' 57 | ) 58 | } 59 | 60 | return partial 61 | } 62 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.3/free_bike_status/required_vehicle_type_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | const partial = { 3 | $id: 'required_vehicle_type_id.json#' 4 | } 5 | 6 | const motorVehicleTypes = vehicleTypes.filter(vt => 7 | ['electric_assist', 'electric', 'combustion'].includes(vt.propulsion_type) 8 | ) 9 | 10 | if (motorVehicleTypes.length) { 11 | partial.$merge = { 12 | source: { 13 | $ref: 14 | 'https://github.com/MobilityData/gbfs/blob/v2.3/gbfs.md#free_bike_statusjson' 15 | }, 16 | with: { 17 | properties: { 18 | data: { 19 | properties: { 20 | bikes: { 21 | items: { 22 | errorMessage: { 23 | required: { 24 | vehicle_type_id: 25 | "'vehicle_type_id' is required for this vehicle type" 26 | } 27 | }, 28 | if: { 29 | properties: { 30 | vehicle_type_id: { 31 | enum: motorVehicleTypes.map(vt => vt.vehicle_type_id) 32 | } 33 | }, 34 | // "required" so it only trigger "then" when "vehicle_type_id" is present. 35 | required: ['vehicle_type_id'] 36 | }, 37 | then: { 38 | required: ['current_range_meters'] 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | partial.$patch = { 50 | source: { 51 | $ref: 52 | 'https://github.com/MobilityData/gbfs/blob/v2.3/gbfs.md#free_bike_statusjson' 53 | }, 54 | with: [ 55 | { 56 | op: 'add', 57 | path: '/properties/data/properties/bikes/items/required/0', 58 | value: 'vehicle_type_id' 59 | } 60 | ] 61 | } 62 | 63 | return partial 64 | } 65 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.3/station_status/required_vehicle_types_available.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | return { 3 | $id: 'required_vehicle_types_available.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v2.3/gbfs.md#station_statusjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | stations: { 14 | items: { 15 | properties: { 16 | vehicle_types_available: { 17 | items: { 18 | properties: { 19 | vehicle_type_id: { 20 | enum: vehicleTypes.map(vt => vt.vehicle_type_id) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | $patch: { 34 | source: { 35 | $ref: 36 | 'https://github.com/MobilityData/gbfs/blob/v2.3/gbfs.md#station_statusjson' 37 | }, 38 | with: [ 39 | { 40 | op: 'add', 41 | path: '/properties/data/properties/stations/items/required/0', 42 | value: 'vehicle_types_available' 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.3/system_information/required_store_uri.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ android = false, ios = false }) => { 2 | const partial = { 3 | $id: 'required_ios_store_uri.json#', 4 | $patch: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v2.3/gbfs.md#system_informationjson' 8 | }, 9 | with: [ 10 | { 11 | op: 'add', 12 | path: '/properties/data/required/0', 13 | value: 'rental_apps' 14 | } 15 | ] 16 | }, 17 | $merge: { 18 | source: { 19 | $ref: 20 | 'https://github.com/MobilityData/gbfs/blob/v2.3/gbfs.md#system_informationjson' 21 | }, 22 | with: { 23 | properties: { 24 | data: { 25 | properties: { 26 | rental_apps: { 27 | required: [], 28 | properties: { 29 | ios: { 30 | required: [] 31 | }, 32 | android: { 33 | required: [] 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (ios) { 45 | partial.$merge.with.properties.data.properties.rental_apps.required.push('ios') 46 | partial.$merge.with.properties.data.properties.rental_apps.properties.ios.required.push( 47 | 'store_uri' 48 | ) 49 | } 50 | 51 | if (android) { 52 | partial.$merge.with.properties.data.properties.rental_apps.required.push( 53 | 'android' 54 | ) 55 | partial.$merge.with.properties.data.properties.rental_apps.properties.android.required.push( 56 | 'store_uri' 57 | ) 58 | } 59 | 60 | return partial 61 | } 62 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v2.3/vehicle_types/pricing_plan_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ pricingPlans }) => { 2 | return { 3 | $id: 'pricing_plan_id.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v2.3/gbfs.md#vehicle_typesjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | vehicle_types: { 14 | items: { 15 | properties: { 16 | default_pricing_plan_id: { 17 | enum: pricingPlans.map(p => p.plan_id) 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.0/station_status/required_vehicle_types_available.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | return { 3 | $id: 'required_vehicle_types_available.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#station_statusjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | stations: { 14 | items: { 15 | properties: { 16 | vehicle_types_available: { 17 | items: { 18 | properties: { 19 | vehicle_type_id: { 20 | enum: vehicleTypes.map(vt => vt.vehicle_type_id) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | $patch: { 34 | source: { 35 | $ref: 36 | 'https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#station_statusjson' 37 | }, 38 | with: [ 39 | { 40 | op: 'add', 41 | path: '/properties/data/properties/stations/items/required/0', 42 | value: 'vehicle_types_available' 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.0/system_information/required_store_uri.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ android = false, ios = false }) => { 2 | const partial = { 3 | $id: 'required_ios_store_uri.json#', 4 | $patch: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#system_informationjson' 8 | }, 9 | with: [ 10 | { 11 | op: 'add', 12 | path: '/properties/data/required/0', 13 | value: 'rental_apps' 14 | } 15 | ] 16 | }, 17 | $merge: { 18 | source: { 19 | $ref: 20 | 'https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#system_informationjson' 21 | }, 22 | with: { 23 | properties: { 24 | data: { 25 | properties: { 26 | rental_apps: { 27 | required: [], 28 | properties: { 29 | ios: { 30 | required: [] 31 | }, 32 | android: { 33 | required: [] 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (ios) { 45 | partial.$merge.with.properties.data.properties.rental_apps.required.push('ios') 46 | partial.$merge.with.properties.data.properties.rental_apps.properties.ios.required.push( 47 | 'store_uri' 48 | ) 49 | } 50 | 51 | if (android) { 52 | partial.$merge.with.properties.data.properties.rental_apps.required.push( 53 | 'android' 54 | ) 55 | partial.$merge.with.properties.data.properties.rental_apps.properties.android.required.push( 56 | 'store_uri' 57 | ) 58 | } 59 | 60 | return partial 61 | } 62 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.0/vehicle_status/required_vehicle_type_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | const partial = { 3 | $id: 'required_vehicle_type_id.json#' 4 | } 5 | 6 | const motorVehicleTypes = vehicleTypes.filter(vt => 7 | ['electric_assist', 'electric', 'combustion'].includes(vt.propulsion_type) 8 | ) 9 | 10 | if (motorVehicleTypes.length) { 11 | partial.$merge = { 12 | source: { 13 | $ref: 14 | 'https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#vehicle_statusjson' 15 | }, 16 | with: { 17 | properties: { 18 | data: { 19 | properties: { 20 | vehicles: { 21 | items: { 22 | errorMessage: { 23 | required: { 24 | vehicle_type_id: 25 | "'vehicle_type_id' is required for this vehicle type" 26 | } 27 | }, 28 | if: { 29 | properties: { 30 | vehicle_type_id: { 31 | enum: motorVehicleTypes.map(vt => vt.vehicle_type_id) 32 | } 33 | }, 34 | // "required" so it only trigger "then" when "vehicle_type_id" is present. 35 | required: ['vehicle_type_id'] 36 | }, 37 | then: { 38 | required: ['current_range_meters'] 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | partial.$patch = { 50 | source: { 51 | $ref: 52 | 'https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#vehicle_statusjson' 53 | }, 54 | with: [ 55 | { 56 | op: 'add', 57 | path: '/properties/data/properties/vehicles/items/required/0', 58 | value: 'vehicle_type_id' 59 | } 60 | ] 61 | } 62 | 63 | return partial 64 | } 65 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.0/vehicle_types/pricing_plan_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ pricingPlans }) => { 2 | return { 3 | $id: 'pricing_plan_id.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v3.0/gbfs.md#vehicle_typesjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | vehicle_types: { 14 | items: { 15 | properties: { 16 | default_pricing_plan_id: { 17 | enum: pricingPlans.map(p => p.plan_id) 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.1-RC/station_status/required_vehicle_types_available.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | return { 3 | $id: 'required_vehicle_types_available.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#station_statusjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | stations: { 14 | items: { 15 | properties: { 16 | vehicle_types_available: { 17 | items: { 18 | properties: { 19 | vehicle_type_id: { 20 | enum: vehicleTypes.map(vt => vt.vehicle_type_id) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | $patch: { 34 | source: { 35 | $ref: 36 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#station_statusjson' 37 | }, 38 | with: [ 39 | { 40 | op: 'add', 41 | path: '/properties/data/properties/stations/items/required/0', 42 | value: 'vehicle_types_available' 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.1-RC/system_information/required_store_uri.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ android = false, ios = false }) => { 2 | const partial = { 3 | $id: 'required_ios_store_uri.json#', 4 | $patch: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#system_informationjson' 8 | }, 9 | with: [ 10 | { 11 | op: 'add', 12 | path: '/properties/data/required/0', 13 | value: 'rental_apps' 14 | } 15 | ] 16 | }, 17 | $merge: { 18 | source: { 19 | $ref: 20 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#system_informationjson' 21 | }, 22 | with: { 23 | properties: { 24 | data: { 25 | properties: { 26 | rental_apps: { 27 | required: [], 28 | properties: { 29 | ios: { 30 | required: [] 31 | }, 32 | android: { 33 | required: [] 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (ios) { 45 | partial.$merge.with.properties.data.properties.rental_apps.required.push('ios') 46 | partial.$merge.with.properties.data.properties.rental_apps.properties.ios.required.push( 47 | 'store_uri' 48 | ) 49 | } 50 | 51 | if (android) { 52 | partial.$merge.with.properties.data.properties.rental_apps.required.push( 53 | 'android' 54 | ) 55 | partial.$merge.with.properties.data.properties.rental_apps.properties.android.required.push( 56 | 'store_uri' 57 | ) 58 | } 59 | 60 | return partial 61 | } 62 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.1-RC/vehicle_status/required_vehicle_type_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ vehicleTypes }) => { 2 | const partial = { 3 | $id: 'required_vehicle_type_id.json#' 4 | } 5 | 6 | const motorVehicleTypes = vehicleTypes.filter(vt => 7 | ['electric_assist', 'electric', 'combustion'].includes(vt.propulsion_type) 8 | ) 9 | 10 | if (motorVehicleTypes.length) { 11 | partial.$merge = { 12 | source: { 13 | $ref: 14 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#vehicle_statusjson' 15 | }, 16 | with: { 17 | properties: { 18 | data: { 19 | properties: { 20 | vehicles: { 21 | items: { 22 | errorMessage: { 23 | required: { 24 | vehicle_type_id: 25 | "'vehicle_type_id' is required for this vehicle type" 26 | } 27 | }, 28 | if: { 29 | properties: { 30 | vehicle_type_id: { 31 | enum: motorVehicleTypes.map(vt => vt.vehicle_type_id) 32 | } 33 | }, 34 | // "required" so it only trigger "then" when "vehicle_type_id" is present. 35 | required: ['vehicle_type_id'] 36 | }, 37 | then: { 38 | required: ['current_range_meters'] 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | partial.$patch = { 50 | source: { 51 | $ref: 52 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#vehicle_statusjson' 53 | }, 54 | with: [ 55 | { 56 | op: 'add', 57 | path: '/properties/data/properties/vehicles/items/required/0', 58 | value: 'vehicle_type_id' 59 | } 60 | ] 61 | } 62 | 63 | return partial 64 | } 65 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.1-RC/vehicle_types/default_reserve_time_require.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ pricingPlansIdsWithReservationPrice }) => { 2 | const partial = { 3 | $id: 'pricing_plan_id.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#vehicle_typesjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | vehicle_types: { 14 | items: { 15 | // allOf avoids overwriting the existing if statement about max_range 16 | "allOf": [ 17 | { 18 | if: { 19 | "anyOf": [ 20 | { 21 | properties: { 22 | default_pricing_plan_id: { 23 | enum: pricingPlansIdsWithReservationPrice 24 | } 25 | } 26 | }, 27 | { 28 | properties: { 29 | pricing_plan_ids: { 30 | contains: { 31 | enum: pricingPlansIdsWithReservationPrice 32 | } 33 | } 34 | } 35 | } 36 | ] 37 | }, 38 | then: { 39 | required: ['default_reserve_time'] 40 | } 41 | } 42 | ] 43 | }, 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | return partial; 53 | } 54 | -------------------------------------------------------------------------------- /gbfs-validator/versions/partials/v3.1-RC/vehicle_types/pricing_plan_id.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ pricingPlans }) => { 2 | const partial = { 3 | $id: 'pricing_plan_id.json#', 4 | $merge: { 5 | source: { 6 | $ref: 7 | 'https://github.com/MobilityData/gbfs/blob/v3.1-RC/gbfs.md#vehicle_typesjson' 8 | }, 9 | with: { 10 | properties: { 11 | data: { 12 | properties: { 13 | vehicle_types: { 14 | items: { 15 | properties: { 16 | default_pricing_plan_id: { 17 | enum: pricingPlans.map(p => p.plan_id) 18 | } 19 | }, 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | return partial; 30 | } 31 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v1.0.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: false, 3 | files: options => { 4 | return [ 5 | { file: 'system_information', required: true }, 6 | { file: 'station_information', required: options.docked }, 7 | { file: 'station_status', required: options.docked }, 8 | { file: 'free_bike_status', required: options.freefloating }, 9 | { file: 'system_hours', required: false }, 10 | { file: 'system_calendar', required: false }, 11 | { file: 'system_regions', required: false }, 12 | { file: 'system_pricing_plans', required: false }, 13 | { file: 'system_alerts', required: false } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v1.1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: false, 3 | files: options => { 4 | return [ 5 | { file: 'gbfs_versions', required: false }, 6 | { file: 'system_information', required: true }, 7 | { file: 'station_information', required: options.docked }, 8 | { file: 'station_status', required: options.docked }, 9 | { file: 'free_bike_status', required: options.freefloating }, 10 | { file: 'system_hours', required: false }, 11 | { file: 'system_calendar', required: false }, 12 | { file: 'system_regions', required: false }, 13 | { file: 'system_pricing_plans', required: false }, 14 | { file: 'system_alerts', required: false } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v2.0.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: true, 3 | files: options => { 4 | return [ 5 | { file: 'gbfs_versions', required: false }, 6 | { file: 'system_information', required: true }, 7 | { file: 'station_information', required: options.docked }, 8 | { file: 'station_status', required: options.docked }, 9 | { file: 'free_bike_status', required: options.freefloating }, 10 | { file: 'system_hours', required: false }, 11 | { file: 'system_calendar', required: false }, 12 | { file: 'system_regions', required: false }, 13 | { file: 'system_pricing_plans', required: false }, 14 | { file: 'system_alerts', required: false } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v2.1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: true, 3 | files: options => { 4 | return [ 5 | { file: 'gbfs_versions', required: false }, 6 | { file: 'system_information', required: true }, 7 | { file: 'vehicle_types', required: false }, // @TODO Conditionally REQUIRED complexe 8 | { file: 'station_information', required: options.docked }, 9 | { file: 'station_status', required: options.docked }, 10 | { file: 'free_bike_status', required: options.freefloating }, 11 | { file: 'system_hours', required: false }, 12 | { file: 'system_calendar', required: false }, 13 | { file: 'system_regions', required: false }, 14 | { file: 'system_pricing_plans', required: false }, 15 | { file: 'system_alerts', required: false }, 16 | { file: 'geofencing_zones', required: false } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v2.2.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: true, 3 | files: options => { 4 | return [ 5 | { file: 'gbfs_versions', required: false }, 6 | { file: 'system_information', required: true }, 7 | { file: 'vehicle_types', required: false }, // @TODO Conditionally REQUIRED complexe 8 | { file: 'station_information', required: options.docked }, 9 | { file: 'station_status', required: options.docked }, 10 | { file: 'free_bike_status', required: options.freefloating }, 11 | { file: 'system_hours', required: false }, 12 | { file: 'system_calendar', required: false }, 13 | { file: 'system_regions', required: false }, 14 | { file: 'system_pricing_plans', required: false }, 15 | { file: 'system_alerts', required: false }, 16 | { file: 'geofencing_zones', required: false } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v2.3.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: true, 3 | files: options => { 4 | return [ 5 | { file: 'gbfs_versions', required: false }, 6 | { file: 'system_information', required: true }, 7 | { file: 'vehicle_types', required: false }, // @TODO Conditionally REQUIRED complexe 8 | { file: 'station_information', required: options.docked }, 9 | { file: 'station_status', required: options.docked }, 10 | { file: 'free_bike_status', required: options.freefloating }, 11 | { file: 'system_hours', required: false }, 12 | { file: 'system_calendar', required: false }, 13 | { file: 'system_regions', required: false }, 14 | { file: 'system_pricing_plans', required: false }, 15 | { file: 'system_alerts', required: false }, 16 | { file: 'geofencing_zones', required: false } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v3.0.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: true, 3 | files: options => { 4 | return [ 5 | { file: 'manifest', required: false }, 6 | { file: 'gbfs_versions', required: false }, 7 | { file: 'system_information', required: true }, 8 | { file: 'vehicle_types', required: false }, 9 | { file: 'station_information', required: options.docked }, 10 | { file: 'station_status', required: options.docked }, 11 | { file: 'vehicle_status', required: options.freefloating }, 12 | { file: 'system_regions', required: false }, 13 | { file: 'system_pricing_plans', required: false }, 14 | { file: 'system_alerts', required: false }, 15 | { file: 'geofencing_zones', required: false } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gbfs-validator/versions/v3.1-RC.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gbfsRequired: true, 3 | files: options => { 4 | return [ 5 | { file: 'manifest', required: false }, 6 | { file: 'gbfs_versions', required: false }, 7 | { file: 'system_information', required: true }, 8 | { file: 'vehicle_types', required: false }, 9 | { file: 'station_information', required: options.docked }, 10 | { file: 'station_status', required: options.docked }, 11 | { file: 'vehicle_status', required: options.freefloating }, 12 | { file: 'system_regions', required: false }, 13 | { file: 'system_pricing_plans', required: false }, 14 | { file: 'system_alerts', required: false }, 15 | { file: 'geofencing_zones', required: false } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "yarn build" 3 | publish = "website/dist" 4 | 5 | [build.environment] 6 | YARN_FLAGS = "--frozen-lockfile --ignore-scripts" 7 | 8 | [functions] 9 | directory = "functions" 10 | 11 | [dev] 12 | command = "yarn run dev:website" 13 | autoLaunch = false 14 | functions = "functions" 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev:website": "cd website && yarn run dev", 5 | "dev": "netlify dev", 6 | "start": "yarn run dev", 7 | "lint": "eslint --ext .js,.vue website/src", 8 | "build": "yarn workspace website build", 9 | "build:website": "cd website && yarn run build", 10 | "postinstall": "git submodule update --init --recursive" 11 | }, 12 | "devDependencies": { 13 | "eslint": "^8.41.0", 14 | "eslint-config-prettier": "^8.8.0", 15 | "eslint-friendly-formatter": "^4.0.1", 16 | "eslint-loader": "^4.0.2", 17 | "eslint-plugin-import": "^2.27.5", 18 | "eslint-plugin-node": "^11.1.0", 19 | "eslint-plugin-promise": "^6.1.1", 20 | "eslint-plugin-vue": "^9.14.1", 21 | "netlify-cli": "^15.11.0" 22 | }, 23 | "workspaces": { 24 | "packages": [ 25 | "website", 26 | "gbfs-validator", 27 | "check-systems", 28 | "functions" 29 | ], 30 | "nohoist": [ 31 | "netlify-cli" 32 | ] 33 | }, 34 | "engines": { 35 | "npm": ">=8.0.0", 36 | "node": ">=18.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | optimization: { minimize: false } 3 | } 4 | -------------------------------------------------------------------------------- /website/.env.exemple: -------------------------------------------------------------------------------- 1 | # Mapbox token (Required for visualization / public scopes) 2 | VITE_MAPBOX_API_KEY= 3 | 4 | # Google Analytics ID (Optionnal) 5 | VITE_GOOGLE_ANALYTICS_ID= 6 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # my-project 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | GBFS-Validator - A GBFS feed validator 18 | 19 | 20 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Website for GBFS validator", 6 | "author": "Pierrick ", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 12 | "format": "prettier --write src/", 13 | "test": "echo \"Error: no test specified\" && exit 0" 14 | }, 15 | "dependencies": { 16 | "@deck.gl/core": "^8.9.18", 17 | "@deck.gl/layers": "^8.9.18", 18 | "@deck.gl/mapbox": "^8.9.18", 19 | "@turf/bbox": "^6.5.0", 20 | "@vue/compat": "^3.3.2", 21 | "@vueuse/core": "^10.1.2", 22 | "bootstrap-vue": "^2.23.1", 23 | "maplibre-gl": "^3.0.1", 24 | "maplibregl-mapbox-request-transformer": "^0.0.2", 25 | "mitt": "^3.0.0", 26 | "sass": "^1.34.1", 27 | "sass-loader": "^12.0.0", 28 | "vue": "^3.3.2", 29 | "vue-gtag": "1.16.1", 30 | "vue-router": "^4.2.2" 31 | }, 32 | "devDependencies": { 33 | "@rushstack/eslint-patch": "^1.2.0", 34 | "@vitejs/plugin-vue": "^4.2.3", 35 | "@vue/eslint-config-prettier": "^7.1.0", 36 | "eslint": "^8.39.0", 37 | "eslint-plugin-vue": "^9.11.0", 38 | "prettier": "^2.8.8", 39 | "vite": "^4.5.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /website/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /website/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/apple-touch-icon.png -------------------------------------------------------------------------------- /website/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #1f8287 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /website/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/favicon-32x32.png -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/gbfs_validator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/gbfs_validator.jpg -------------------------------------------------------------------------------- /website/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MobilityData/gbfs-validator/d5dc7019358d9c59019a8afc3ee4ffec05258705/website/public/mstile-150x150.png -------------------------------------------------------------------------------- /website/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 33 | 36 | 50 | 64 | 69 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /website/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GBFS validator", 3 | "short_name": "GBFS validator", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /website/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 61 | 62 | 71 | -------------------------------------------------------------------------------- /website/src/components/DownloadSchema.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 48 | -------------------------------------------------------------------------------- /website/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /website/src/components/GeofencingZonePopup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 40 | 41 | 58 | -------------------------------------------------------------------------------- /website/src/components/Result.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 63 | 64 | 79 | -------------------------------------------------------------------------------- /website/src/components/StationPopup.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | 60 | -------------------------------------------------------------------------------- /website/src/components/SubResult.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 83 | -------------------------------------------------------------------------------- /website/src/components/VehiclePopup.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 68 | 69 | 86 | -------------------------------------------------------------------------------- /website/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueGtag from 'vue-gtag' 3 | import App from './App.vue' 4 | import { 5 | AlertPlugin, 6 | ButtonPlugin, 7 | CardPlugin, 8 | CollapsePlugin, 9 | FormCheckboxPlugin, 10 | FormGroupPlugin, 11 | FormInputPlugin, 12 | FormPlugin, 13 | FormSelectPlugin, 14 | LayoutPlugin, 15 | LinkPlugin, 16 | TabsPlugin, 17 | TooltipPlugin 18 | } from 'bootstrap-vue' 19 | 20 | import 'bootstrap/dist/css/bootstrap.css' 21 | import 'bootstrap-vue/dist/bootstrap-vue.css' 22 | 23 | import 'maplibre-gl/dist/maplibre-gl.css' 24 | 25 | import router from './router' 26 | 27 | if (import.meta.env.VITE_GOOGLE_ANALYTICS_ID) { 28 | Vue.use(VueGtag, { 29 | config: { id: import.meta.env.VITE_GOOGLE_ANALYTICS_ID } 30 | }) 31 | } 32 | 33 | Vue.config.productionTip = false 34 | Vue.use(AlertPlugin) 35 | Vue.use(ButtonPlugin) 36 | Vue.use(CardPlugin) 37 | Vue.use(CollapsePlugin) 38 | Vue.use(FormCheckboxPlugin) 39 | Vue.use(FormGroupPlugin) 40 | Vue.use(FormInputPlugin) 41 | Vue.use(FormPlugin) 42 | Vue.use(FormSelectPlugin) 43 | Vue.use(LayoutPlugin) 44 | Vue.use(LinkPlugin) 45 | Vue.use(TabsPlugin) 46 | Vue.use(TooltipPlugin) 47 | 48 | const app = Vue.createApp(App) 49 | 50 | app.use(router) 51 | app.mount('#app') 52 | -------------------------------------------------------------------------------- /website/src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /website/src/pages/Validator.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 270 | -------------------------------------------------------------------------------- /website/src/pages/Visualization.vue: -------------------------------------------------------------------------------- 1 | 467 | 468 | 569 | 570 | 666 | 667 | 679 | -------------------------------------------------------------------------------- /website/src/router.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Home from './pages/Home.vue' 3 | import Validator from './pages/Validator.vue' 4 | import Visualization from './pages/Visualization.vue' 5 | 6 | const routes = [ 7 | { name: 'home', path: '/', component: Home }, 8 | { name: 'validator', path: '/validator', component: Validator }, 9 | { name: 'visualization', path: '/visualization', component: Visualization } 10 | ] 11 | 12 | const router = createRouter({ 13 | history: createWebHistory(), 14 | routes 15 | }) 16 | 17 | export default router 18 | -------------------------------------------------------------------------------- /website/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | server: { 9 | port: 8080, 10 | proxy: { 11 | '^/.netlify/functions/.*': 'http://localhost:8888' 12 | } 13 | }, 14 | plugins: [vue()], 15 | resolve: { 16 | alias: { 17 | '@': fileURLToPath(new URL('./src', import.meta.url)), 18 | vue: '@vue/compat' 19 | } 20 | } 21 | }) 22 | --------------------------------------------------------------------------------