diff --git a/api-linter/.npmignore b/api-linter/.npmignore new file mode 100644 index 0000000..f2bcf5c --- /dev/null +++ b/api-linter/.npmignore @@ -0,0 +1,3 @@ +/.npm-cache +/test +/example \ No newline at end of file diff --git a/api-linter/README.md b/api-linter/README.md new file mode 100644 index 0000000..8243924 --- /dev/null +++ b/api-linter/README.md @@ -0,0 +1,171 @@ +# Siemens API Linter + +This project implements a linter reporter for the [Stoplight Spectral](https://docs.stoplight.io/docs/spectral/674b27b261c3c-overview) for API specifications. +The ruleset has been created based on the [Siemens Xcelerator API guidelines 2.5.0](https://developer.siemens.com/guidelines/api-guidelines/rest/index.html). + +## Getting started +### Directory structure + +- `example/`: contains a sample API spec file +- `rulesets/`: contains the ruleset files those follow the Siemens Xcelerator API guidelines +- `src/`: contains the source files for creating the linter report +- `test/`: jest test cases for different rules in the rulesets + +### Installation + +In order to include the rulesets and provide linting within your project, you can install the package from this project with this `@siemens/api-linter` package. + +```json +{ + "scripts": { + "test": "api-linter -s reference-api-specs/openapi.yml -r reference-api-specs/.spectral.yml" + }, + "devDependencies": { + "@siemens/api-linter": "^0.8.4" + } +} +``` + +The `@siemens/api-linter` package includes the new Siemens REST API linter tool with html report capability. + +In order to trigger the linter on your API specifications, it provides a script to execute the linter within the `package.json` specification. + +```json + "scripts": { + "test": "api-linter -s reference-api-specs/openapi.yml -r reference-api-specs/.spectral.yml" + } +``` +Developers can change the spec file path with `-s` option, ruleset file path with `-r` option accordingly in the scripts.
+Or just remove them and set them in the command line as:
+ +Script: +```json + "scripts": { + "test": "api-linter" + } +``` +Command: +```groovy +npm test -s reference-api-specs/openapi.yml -r reference-api-specs/.spectral.yml +``` + +For more options: + +``` +Usage: api-linter -s "path-to-spec-file" -r "path-to-rule-file" [-f "fail-severity"] [-c "console-severity"] [-v "api-versioning"] [-a "api-security"] + +Options: + -s, --specPath path to openapi specification + -r, --rulesetPath path to rules file + -f, --failSeverity test fails when met result the severity equal or higher than it (choices: "error", "warn", "info", "hint", default: warn) + -c, --consoleSeverity console output message for linter result no matter job failed or succeeded (choices: "error", "warn", "info", "hint", default: warn) + -o, --outputFilename specify the output filename with or without .html extension (default: linter-result.html) + -p, --jsonFile , output json file with origin spectral result data + -v, --apiVersioning the api versioning way (choices: "ignore", "url", "header", default: ignore) + -a, --apiSecurity if enable authorization security (choices: "n/f/no/false/y/t/yes/true" default: n) + --resolve enables follow external $refs (same as Spectral --resolve) (default is not enabled, i.e. external references are not resolved and ignored) + ``` + +A linter report will be generated according to the execution as: + +![Example Image](linter-report.png) + +### Linting within CI/CD + +You can trigger linting on your CI/CD pipeline executing `npm test` within the `.gitlab-ci.yml` CI configuration file. + +See the following configuration example: + +```yaml +build: + script: + - npm test + artifacts: + name: "api-linter-report" + paths: + - linter-result.html + when: always +``` + +> The Pipeline will fail if linter results with severity of `warn` (By default) or higher found! + +> You may download or browse the report file in the job artifact page directly no matter CI job failed or succeeded. + +### Selection of rulesets + +The package provides several rulesets according to Siemens REST API guidelines.
+> They can be included as needed by providing a project specific `.spectral.yml` file on the projects root directory as follows: +```yaml +extends: + - "@siemens/api-linter/rulesets/siemens-api-media-type.yml" + - "@siemens/api-linter/rulesets/siemens-api-versioning.yml" + - "@siemens/api-linter/rulesets/siemens-api-error-reporting.yml" + - "@siemens/api-linter/rulesets/siemens-api-filtering.yml" + - "@siemens/api-linter/rulesets/siemens-api-sparse-fieldsets.yml" + - "@siemens/api-linter/rulesets/siemens-api-pagination.yml" + - "@siemens/api-linter/rulesets/siemens-api-sorting.yml" + - "@siemens/api-linter/rulesets/siemens-api-common-operation.yml" + - "@siemens/api-linter/rulesets/siemens-api-security.yml" +``` +> You MAY use as below to achieve this for guideline linting `WITHOUT` `"spectral:oas"`. +```yaml +extends: + - "@siemens/api-linter/rulesets/siemens-api-express.yml" +``` +> `OR` just extends the `ALL-IN-ONE` ruleset which with `"spectral:oas"` enabled. +```yaml +extends: + - "@siemens/api-linter/rulesets/siemens-api.yml" +``` +### Disable any Ruleset or Rules +> You can disable any rules/rulesets as you wish by [Stoplight Spectral](https://docs.stoplight.io/docs/spectral/674b27b261c3c-overview). +```yaml +#e.g. disable spectral:oas ruleset and disable Siemens-API-[400] rules from the extended ruleset +extends: + - "@siemens/api-linter/rulesets/siemens-api.yml" + - ["spectral:oas", "off"] + +rules: + Siemens-API-[400]: false + Siemens-API-[401]: false +``` + +### Define rule dependency relations +> Sometimes, if you don't want to follow a ruleC that under a high level ruleP, which means when ruleP failed, you don't want to check ruleC. +Then we can define the rules with `"x-dependsOn"` attribute. +```yaml +#e.g. Siemens-API-[101.8.1] depends on Siemens-API-[101.8] +extends: + - "@siemens/api-linter/rulesets/siemens-api.yml" + +rules: + Siemens-API-[101.8.1]: + x-dependsOn: + - Siemens-API-[101.8] +``` + +## Integration Using JavaScript +> In case you want to handle the linting results by yourself +```javascript +const {validator} = require('@siemens/api-linter/src/extension'); +const {DiagnosticSeverity} = require('@stoplight/types'); +const path = require("path"); +const validate = validator(); + +const validateDoc = function(){ + const ruleSetFilePath = path.resolve(__dirname, 'reference-api-specs/.spectral.yml'); + const apiSpecFilePath = path.resolve(__dirname, 'reference-api-specs/openapi-prod.yml'); + validate(apiSpecFilePath, ruleSetFilePath).then(result => { + const diagnostics = []; + for (let msg of result.results || []){ + if (msg.severity <= DiagnosticSeverity.Information ){ + var diagnostic = { value: msg.code, severity: msg.severity, message: msg.message }; + diagnostics.push(diagnostic); + } + } + // Add your own processes to handle diagnostic array; + }); +} +validateDoc(); +``` + diff --git a/api-linter/example/openapi.yml b/api-linter/example/openapi.yml new file mode 100644 index 0000000..f90ed5d --- /dev/null +++ b/api-linter/example/openapi.yml @@ -0,0 +1,1525 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. + + The API models an educational system. It allows to manage courses, their + individual lessons, the rooms where lessons take place as well as people + acting as teachers and/or participants of courses. + + **Feature Guide** + + Different parts in this API illustrate different aspects and complexities of the API guidelines: + + * Complexity: Courses endpoints try to showcase the full capabilities of REST API. However, also + simple APIs can be modeled with REST API as shown with the Rooms endpoints. + * Pagination approaches: Courses and and People resource collections use index-based + pagination. Rooms use cursor-based pagination. Messages use offset-based pagination. + * Added API version either in URI path or custom request header are supported. + * Added self links example for hypermedia controls following Maturity Model. + * Removed all REST-specific rules which were based on the JSON:API guidelines from this version. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. + - name: Courses + description: | + Courses consist of lessons and have participants and teachers assigned. + Participants can be assigned and de-assigned from a course in batch by using + the relationships endpoint. Teachers can be added or removed from a course + only via the course's update method. + - name: Lessons + description: | + Lessons describe the concrete occurrences of the lectures of a course. Each + lesson is part of a course and gets deleted when that course is deleted. + A lesson describes one regular time slot for a course and relates to the + corresponding room. Lessons can be retrieved as included relationships of + courses or rooms. + - name: People + description: | + People can participate or teach courses. The assignment as participant or + teacher is done at the corresponding course resource. +paths: + /courses: + post: + summary: Creates a Course + description: | + Create a Course with the request content by this post action. + operationId: CreateCourse + tags: + - Courses + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CourseCreationRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/CourseCreationResponse" + default: + $ref: "#/components/responses/DefaultErrors" + get: + summary: Queries Courses + description: | + Query a collection of Courses with the request by this get action. + operationId: ReadCourses + tags: + - Courses + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/CoursesReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" + parameters: + - name: fields + in: query + description: Returns only provided fields (attributes or relationships) for resource Course. + required: false + schema: + type: string + default: id,name + - name: number + in: query + description: Page number, starting with index 0. + schema: + type: integer + default: 0 + - name: size + in: query + description: Desired number of elements to be returned per page. A server may return less elements. + schema: + type: integer + default: 1 + - name: sort + in: query + description: Sorts resources returned in response according to values of provided attribute. Default direction is ascending. Descending direction is denoted by prepending a - to the attribute name. + required: false + schema: + type: string + enum: + - id + - name + "/courses/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Course resource. + schema: + type: string + example: 302492f2-7a65-4b90-b2b4-065d4e25d4d2 + get: + summary: Returns a Course + description: | + Query the specified Course according to the id parameter by this get action. + operationId: ReadCourse + tags: + - Courses + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/CourseReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" + parameters: + - name: fields + in: query + description: Returns only provided fields (attributes or relationships) for resource Course. + required: false + schema: + type: string + default: id,name,startDate,endDate + patch: + summary: Updates a Course + description: | + Update the specified Course according to the id parameter by this patch action. + operationId: UpdateCourse + tags: + - Courses + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CourseUpdateRequest" + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/CourseUpdateResponse" + default: + $ref: "#/components/responses/DefaultErrors" + delete: + summary: Deletes a Course + description: | + Delete the specified Course according to the id parameter by this delete action. + operationId: DeleteCourse + tags: + - Courses + responses: + "204": + description: No content + default: + $ref: "#/components/responses/DefaultErrors" + "/courses/{id}/lessons": + parameters: + - name: id + in: path + required: true + description: Identifier of a Course resource. + schema: + type: string + example: 541edcc2-f21a-4328-8ca4-c2d56823635c + get: + summary: Returns the Lessons of the Course + description: | + Query the related Lessons of the specified Course according to the id parameter by this get action. + operationId: ReadCourseRelatedLessons + tags: + - Courses + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/CourseRelatedLessonsResponse" + default: + $ref: "#/components/responses/DefaultErrors" + "/courses/{id}/participants": + parameters: + - name: id + in: path + required: true + description: Identifier of a Course resource. + schema: + type: string + example: ce249dd4-2215-45bb-8f52-e447192ef3a7 + post: + summary: Adds a Person to the Course + description: | + Add People with the participant relation to the specified Course according to the id parameter by this post action. + operationId: AddCourseParticipants + tags: + - Courses + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CourseParticipantsRequest" + responses: + "204": + description: No content + default: + $ref: "#/components/responses/DefaultErrors" + get: + summary: Returns the Person from the Course + description: | + Query the People with participant relation of the specified Course according to the id parameter by this get action. + operationId: ReadCourseParticipants + tags: + - Courses + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/CourseParticipantsResponse" + default: + $ref: "#/components/responses/DefaultErrors" + parameters: + - name: number + in: query + description: Page number, starting with index 0. + schema: + type: integer + default: 1 + - name: size + in: query + description: Desired number of elements to be returned per page. A server may return less elements. + schema: + type: integer + default: 1 + - name: fields + in: query + description: Returns only provided fields (attributes or relationships) for resource Course. + required: false + schema: + type: string + default: id,name,email + - name: sort + in: query + description: Sorts resources returned in response according to values of provided attribute. Default direction is ascending. Descending direction is denoted by prepending a - to the attribute name. + required: false + schema: + type: string + enum: + - id + - name + - email + delete: + summary: Removes the Person from the Course + description: | + Remove the People with participant relation of the specified Course according to the id parameter by this delete action. + operationId: RemoveCourseParticipants + tags: + - Courses + responses: + "204": + description: No content + default: + $ref: "#/components/responses/DefaultErrors" + "/courses/{id}/teacher": + parameters: + - name: id + in: path + required: true + description: Identifier of a Course resource. + schema: + type: string + example: 275bd2a1-e6cb-4ec3-80f3-22167c6bcaab + get: + summary: Returns the Teacher of the Course + description: | + Query the People with teacher relation of the specified Course according to the id parameter by this get action. + operationId: ReadCourseTeacher + tags: + - Courses + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/CourseRelatedTeacherResponse" + default: + $ref: "#/components/responses/DefaultErrors" + post: + summary: Adds a Teacher to the Course + description: | + Add People with the teacher relation to the specified Course according to the id parameter by this post action. + operationId: AddCourseTeacher + tags: + - Courses + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CourseRelatedTeacherRequest" + responses: + "204": + description: No content + default: + $ref: "#/components/responses/DefaultErrors" + /lessons: + post: + summary: Creates a Lesson + description: | + Create a Lesson with the request content by this post action. + operationId: CreateLesson + tags: + - Lessons + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LessonCreationRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/LessonCreationResponse" + default: + $ref: "#/components/responses/DefaultErrors" + "/lessons/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Lesson resource. + schema: + type: string + example: c7cafbb8-f4f3-4cb7-818e-389395abf815 + patch: + summary: Updates a Lesson + description: | + Update a Lesson with the request content by this patch action. + operationId: UpdateLesson + tags: + - Lessons + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/LessonUpdateRequest" + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/LessonUpdateResponse" + default: + $ref: "#/components/responses/DefaultErrors" + /people: + post: + summary: Creates a Person + description: | + Create a Person with the request content by this post action. + operationId: CreatePerson + tags: + - People + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PersonCreationRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/PersonCreationResponse" + default: + $ref: "#/components/responses/DefaultErrors" + get: + summary: Queries People + description: | + Query a collection of People with the request by this get action. + operationId: ReadPeople + tags: + - People + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/PeopleReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" + parameters: + - name: fields + in: query + description: Returns only provided fields (attributes or relationships) for resource Course. + required: false + schema: + type: string + default: id,name,email + - name: number + in: query + description: Page number, starting with index 0. + schema: + type: integer + default: 0 + - name: size + in: query + description: Desired number of elements to be returned per page. A server may return less elements. + schema: + type: integer + default: 1 + - name: sort + in: query + description: Sorts resources returned in response according to values of provided attribute. Default direction is ascending. Descending direction is denoted by prepending a - to the attribute name. + required: false + schema: + type: string + enum: + - id + - name + - email + "/people/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Person resource. + schema: + type: string + example: 21ee45c0-ea33-4a81-affc-4e211cc1c6c0 + get: + summary: Returns a Person + description: | + Query the specified Person according to the id parameter by this get action. + operationId: ReadPerson + tags: + - People + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/PersonReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" + parameters: + - name: fields + in: query + description: Returns only provided fields (attributes or relationships) for resource Course. + required: false + schema: + type: string + default: id,name,email + patch: + summary: Updates a Person + description: | + Update the specified Person according to the id parameter by this patch action. + operationId: UpdatePerson + tags: + - People + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/PersonUpdateRequest" + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/PersonUpdateResponse" + default: + $ref: "#/components/responses/DefaultErrors" + delete: + summary: Deletes a Person + description: | + Delete the specified Person according to the id parameter by this delete action. + operationId: DeletePerson + tags: + - People + responses: + "204": + description: No content + default: + $ref: "#/components/responses/DefaultErrors" + "/people/{id}/participated-courses": + parameters: + - name: id + in: path + required: true + description: Identifier of a Person resource. + schema: + type: string + example: 5428aa10-95ff-478e-b72c-83496c2df4d9 + get: + summary: Returns related Courses + description: | + Query the Courses particifated by the specified Person according to the id parameter by this get action. + operationId: ReadPersonRelatedParticipatedCourses + tags: + - People + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/PersonRelatedParticipatedCoursesResponse" + default: + $ref: "#/components/responses/DefaultErrors" + "/people/{id}/taught-courses": + parameters: + - name: id + in: path + required: true + description: Identifier of a Person resource. + schema: + type: string + example: 0cb649e5-2d08-4e12-9fef-53968b716df5 + get: + summary: Returns related Courses + description: | + Query the Courses taught by the specified Person according to the id parameter by this get action. + operationId: ReadPersonRelatedTaughtCourses + tags: + - People + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/PersonRelatedTaughtCoursesResponse" + default: + $ref: "#/components/responses/DefaultErrors" + /rooms: + post: + summary: Creates a Room + description: | + Create a Room with the request content by this post action. + operationId: CreateRoom + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationResponse" + default: + $ref: "#/components/responses/DefaultErrors" + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" + parameters: + - name: fields + in: query + description: Returns only provided fields (attributes or relationships) for resource Course. + required: false + schema: + type: string + default: id,code,creationDate + - name: number + in: query + description: Opaque cursor to fetch specific page + schema: + type: integer + default: 1 + - name: size + in: query + description: Desired maximum number of elements to be returned. A server may return less elements. + schema: + type: integer + default: 1 + - name: sort + in: query + description: Sorts resources returned in response according to values of provided attribute. Default direction is ascending. Descending direction is denoted by prepending a - to the attribute name. + required: false + schema: + type: string + enum: + - id + - code + - creationDate + "/rooms/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Room resource. + schema: + type: string + example: 7eeea381-872c-4b83-a228-31878e5b8de8 + get: + summary: Returns a Room + description: | + Query the specified Room according to the id parameter by this get action. + operationId: ReadRoom + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" + patch: + summary: Updates a Room + description: | + Update the specified Room according to the id parameter by this patch action. + operationId: UpdateRoom + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateRequest" + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateResponse" + default: + $ref: "#/components/responses/DefaultErrors" + delete: + summary: Deletes a Room + description: | + Delete the specified Room according to the id parameter by this delete action. + operationId: DeleteRoom + tags: + - Rooms + responses: + "204": + description: No content + default: + $ref: "#/components/responses/DefaultErrors" + "/rooms/{id}/lessons": + parameters: + - name: id + in: path + required: true + description: Identifier of a Room resource. + schema: + type: string + example: d884dcae-fdff-4992-bc0b-22573a5ae4a5 + get: + summary: Returns related Lessons + description: | + Query the related Lessons of the specified Room according to the id parameter by this get action. + operationId: ReadRoomRelatedLessons + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomRelatedLessonsResponse" + default: + $ref: "#/components/responses/DefaultErrors" + parameters: + - name: cursor + in: query + description: Opaque cursor to fetch specific page + schema: + type: string + default: opaque identifier + - name: limit + in: query + description: Desired maximum number of elements to be returned. A server may return less elements. + schema: + type: integer + default: 1 +components: + schemas: + CourseBase: + type: object + description: | + A course represents a scheduled set of lessons, participants, and a teacher related to a specific learning topic. + properties: + name: + type: string + example: Numerical Mathematics I + description: Name of the course. May be not unique. + startDate: + type: string + format: date + example: 2022-02-02 + description: Date when first lesson takes place + endDate: + type: string + format: date + example: 2022-07-27 + description: Date when last lesson takes place + Course: + allOf: + - required: + - id + - type: object + properties: + id: + type: string + example: 822a6549-4fd8-478a-b5ed-73bef7a066f4 + - $ref: "#/components/schemas/CourseBase" + CourseLink: + allOf: + - $ref: "#/components/schemas/CourseBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + CourseCreationRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/CourseBase" + CourseCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/CourseLink" + CourseReadResponse: + type: object + required: + - data + properties: + data: + allOf: + - $ref: "#/components/schemas/CourseBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + participants: + type: string + example: "https://api.siemens.com/reference/api/people/54293c17-a305-4349-81d5-83001e9e4032" + description: "#servers/url" + teacher: + type: string + example: "https://api.siemens.com/reference/api/people/54293c17-a305-4349-81d5-83001e9e4032" + description: "#servers/url" + CourseRelatedLessonsResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/LessonLink" + CourseRelatedTeacherRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/PersonLink" + CourseRelatedTeacherResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/PersonLink" + CourseParticipantsRequest: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: "#/components/schemas/Person" + CourseParticipantsResponse: + type: object + required: + - data + properties: + data: + type: array + items: + $ref: "#/components/schemas/PersonLink" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses/1/participants?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/courses/1/participants?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/courses/1/participants?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/courses/1/participants?page=2&size=1" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + CourseUpdateRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/Course" + CourseUpdateResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/CourseLink" + CoursesReadResponse: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/CourseBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + description: "#servers/url" + participants: + type: string + example: "https://api.siemens.com/reference/api/people/54293c17-a305-4349-81d5-83001e9e4032" + description: "#servers/url" + teacher: + type: string + example: "https://api.siemens.com/reference/api/people/54293c17-a305-4349-81d5-83001e9e4032" + description: "#servers/url" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + LessonBase: + type: object + description: Regularly scheduled occurrence of a course, associated to a room. + properties: + weekday: + type: integer + format: date-wday + pattern: '^[1-7]$' + example: 1 + description: | + Weekday where the lesson takes place, according to [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339#appendix-A). + frequency: + type: string + enum: + - Weekly + - Bi-Weekly + - Monthly + description: How often the lesson takes place + begin: + type: string + format: date + example: 2022-02-07 + description: First time the lesson takes place, according to [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339). + duration: + type: string + format: duration + example: PT1H + description: Duration of the lesson, according to [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339#appendix-A). + Lesson: + allOf: + - required: + - id + - type: object + properties: + id: + type: string + example: d8272c80-62b1-43b3-ba84-02cbe0aab5a2 + - $ref: "#/components/schemas/LessonBase" + LessonLink: + allOf: + - $ref: "#/components/schemas/LessonBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + + LessonCreationRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/LessonBase" + links: + type: object + properties: + course: + type: string + example: "https://api.siemens.com/reference/api/class/822a6549-4fd8-478a-b5ed-73bef7a066f4" + description: "#servers/url" + room: + type: string + example: "https://api.siemens.com/reference/api/room/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + LessonCreationResponse: + type: object + required: + - data + properties: + data: + allOf: + - $ref: "#/components/schemas/LessonLink" + - type: object + properties: + links: + type: object + properties: + course: + type: string + example: "https://api.siemens.com/reference/api/class/822a6549-4fd8-478a-b5ed-73bef7a066f4" + description: "#servers/url" + room: + type: string + example: "https://api.siemens.com/reference/api/room/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + LessonUpdateRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/Lesson" + LessonUpdateResponse: + type: object + required: + - data + properties: + data: + allOf: + - $ref: "#/components/schemas/LessonLink" + - type: object + properties: + links: + type: object + properties: + course: + type: string + example: "https://api.siemens.com/reference/api/class/822a6549-4fd8-478a-b5ed-73bef7a066f4" + description: "#servers/url" + room: + type: string + example: "https://api.siemens.com/reference/api/room/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + RoomPageCursor: + type: object + description: Pagination links for cursor-based pagination. + properties: + self: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + first: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + last: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + next: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + LessonPageCursor: + type: object + description: Pagination links for cursor-based pagination. + properties: + self: + type: string + example: https://api.siemens.com/reference/api/lessons?limit=1 + description: | + Opaque cursor to fetch next elements. To be used as value of query parameter cursor. + Is only added if more elements are present. + next: + type: string + example: https://api.siemens.com/reference/api/lessons?cursor=cXdlcnR5&limit=1 + description: Returned the next elements after the cursor. + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - number + - size + - totalPages + properties: + number: + type: integer + example: 1 + description: Current page number, starting from 1. + size: + type: integer + example: 1 + description: Returned number of elements. + totalPages: + type: integer + example: 100 + description: Total amount of pages available. + elements: + type: integer + example: 100 + description: Amount of elements available. + totalElements: + type: integer + example: 100 + description: Total amount of elements available. + PeopleReadResponse: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/PersonBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/people/54293c17-a305-4349-81d5-83001e9e4032" + description: "#servers/url" + participantdCourses: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + description: "#servers/url" + taughtCourses: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/people?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/people?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/people?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/people?number=2&size=1" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + PersonBase: + type: object + description: A person can participate in or teach courses. + properties: + name: + type: string + example: Jane Doe + description: Full name of the person + email: + type: string + example: jane.doe@xcelerator.com + description: Email context of the person + Person: + allOf: + - required: + - id + - type: object + properties: + id: + type: string + example: 54293c17-a305-4349-81d5-83001e9e4032 + - $ref: "#/components/schemas/PersonBase" + PersonLink: + allOf: + - $ref: "#/components/schemas/PersonBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/people/54293c17-a305-4349-81d5-83001e9e4032" + PersonCreationRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/PersonBase" + PersonCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/PersonLink" + PersonReadResponse: + type: object + required: + - data + properties: + data: + allOf: + - $ref: "#/components/schemas/PersonBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/people/54293c17-a305-4349-81d5-83001e9e4032" + participantdCourses: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + description: "#servers/url" + taughtCourses: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + description: "#servers/url" + PersonRelatedParticipatedCoursesResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/CourseLink" + PersonRelatedTaughtCoursesResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/CourseLink" + PersonUpdateRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/PersonLink" + PersonUpdateResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/PersonLink" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + Room: + allOf: + - required: + - id + - type: object + properties: + id: + type: string + example: 846650de-20fd-4197-867b-00ac7606cffd + - $ref: "#/components/schemas/RoomBase" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + RoomCreationRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomBase" + RoomCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + RoomReadResponse: + type: object + required: + - data + properties: + data: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomRelatedLessonsResponse: + type: object + required: + - data + - meta + properties: + data: + $ref: "#/components/schemas/Lesson" + links: + $ref: "#/components/schemas/LessonPageCursor" + RoomUpdateRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/Room" + RoomUpdateResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + RoomsReadResponse: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + $ref: "#/components/schemas/RoomPageCursor" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "404" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + detail: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. + responses: + DefaultErrors: + description: Standard errors + content: + application/json: + schema: + $ref: "#/components/schemas/Errors" + examples: + Forbidden: + $ref: "#/components/examples/Forbidden" + InternalError: + $ref: "#/components/examples/InternalError" + InvalidBodyProperty: + $ref: "#/components/examples/InvalidBodyProperty" + InvalidBodyPropertyValue: + $ref: "#/components/examples/InvalidBodyPropertyValue" + InvalidParameter: + $ref: "#/components/examples/InvalidParameter" + InvalidParameterValue: + $ref: "#/components/examples/InvalidParameterValue" + TooManyRequests: + $ref: "#/components/examples/TooManyRequests" + Unauthorized: + $ref: "#/components/examples/Unauthorized" + examples: + Forbidden: + summary: forbidden + value: + errors: + - id: 5fa91094-7caf-4ce7-85f5-25f7d883a7d4 + status: "403" + code: forbidden + title: Forbidden + detail: The provided authorization means did not contain suitable permissions. + InternalError: + summary: internalError + value: + errors: + - id: a373b344-3e5c-4e2b-b282-c3808d455501 + status: "500" + code: internalError + title: Internal error + detail: An internal error has occurred. + InvalidBodyProperty: + summary: invalidBodyProperty + value: + errors: + - id: eeb556b9-68be-48a6-b131-c9d744684084 + status: "400" + code: invalidBodyProperty + title: Invalid request body property + detail: Provided request body property '{property}' is invalid. + InvalidBodyPropertyValue: + summary: invalidBodyPropertyValue + value: + errors: + - id: d2755b4b-7397-43d5-a348-0ec1e8b1e31a + status: "400" + code: invalidBodyPropertyValue + title: Invalid request body property value + detail: Provided value '{value}' of request body property '{property}' is invalid. + InvalidParameter: + summary: invalidParameter + value: + errors: + - id: 18d1d99b-5658-4b27-95b5-890973d4a8f1 + status: "400" + code: invalidParameter + title: Invalid request parameter + detail: Provided request parameter '{parameter}' is invalid. + InvalidParameterValue: + summary: invalidParameterValue + value: + errors: + - id: 9fffd3aa-6916-43ad-94f3-42f96775a4cc + status: "400" + code: invalidParameterValue + title: Invalid request parameter value + detail: Provided value '{value}' of request parameter '{parameter}' is invalid. + TooManyRequests: + summary: tooManyRequests + value: + errors: + - id: f774e735-9135-456c-ac4a-00cc2128f19f + status: "429" + code: tooManyRequests + title: Too many requests + detail: The server is temporarily throttling requests from the client. + Unauthorized: + summary: unauthorized + value: + errors: + - id: dcfe5037-3380-4719-89d0-1a613d8a229c + status: "401" + code: unauthorized + title: Unauthorized request + detail: No valid authorization means was provided in the request. diff --git a/api-linter/package.json b/api-linter/package.json new file mode 100644 index 0000000..a729411 --- /dev/null +++ b/api-linter/package.json @@ -0,0 +1,47 @@ +{ + "name": "@siemens/api-linter", + "version": "0.8.4", + "description": "A linter tool with Rulesets follow Siemens REST API Guidelines", + "homepage": "https://github.com/siemens/lint", + "author": { + "name": "Siemens", + "email": "opensource@siemens.com" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/siemens/lint.git" + }, + "keywords": [ + "siemens", + "restapi", + "guidelines", + "lint", + "report" + ], + "main": "./src/index.js", + "bin":{ + "api-linter": "./src/index.js" + }, + "scripts": { + "test": "jest", + "dev": "node src/index.js" + }, + "dependencies": { + "@stoplight/spectral-core": "^1.15.1", + "@stoplight/spectral-ruleset-bundler": "^1.5.2", + "@stoplight/spectral-ref-resolver": "^1.0.4", + "@jamietanna/spectral-test-harness": "^0.3.0", + "handlebars": "^4.7.7", + "commander": "^12.0.0", + "underscore": "^1.13.1", + "standard": "^17.0.0", + "js-yaml": "^4.1.0", + "jsonpath": "^1.1.1" + }, + "devDependencies": { + "jest": "^29.0.0", + "jest-junit": "^16.0.0", + "jsonpath": "^1.1.1" + } +} \ No newline at end of file diff --git a/api-linter/rulesets/functions/assert-http-codes-for-operation.js b/api-linter/rulesets/functions/assert-http-codes-for-operation.js new file mode 100644 index 0000000..bf3b9f2 --- /dev/null +++ b/api-linter/rulesets/functions/assert-http-codes-for-operation.js @@ -0,0 +1,31 @@ +export default (targetValue, { wellUnderstood }, context) => { + const result = []; + if (targetValue === null || typeof targetValue !== 'object') { + return result; + } + for (const verb of Object.keys(targetValue)) { + const responses = targetValue[verb].responses || {}; + if (responses === null || typeof responses !== 'object') { + continue; + } + for (const code of Object.keys(responses)) { + if (!(code in wellUnderstood)) { + result.push({ + message: `${code} is not a well-understood HTTP status code`, + path: [...context.path, verb, 'responses', code], + }); + continue; + } + const allowedVerbs = wellUnderstood[code].map((verb) => verb.toUpperCase()); + const upperCaseVerb = verb.toUpperCase(); + if (!allowedVerbs.includes('ALL') && !allowedVerbs.includes(upperCaseVerb)) { + result.push({ + message: `${code} is not a well-understood HTTP status code for ${upperCaseVerb}`, + path: [...context.path, verb, 'responses', code], + }); + continue; + } + } + } + return result; +}; diff --git a/api-linter/rulesets/functions/count-resource-types.js b/api-linter/rulesets/functions/count-resource-types.js new file mode 100644 index 0000000..f477384 --- /dev/null +++ b/api-linter/rulesets/functions/count-resource-types.js @@ -0,0 +1,19 @@ +'use strict'; + +const extractResourceTypeFromPath = (path) => { + return path.split('/')[path.startsWith('/') ? 1 : 0]; +}; + +export default (targetValue, { max }) => { + const paths = Object.keys(targetValue); + if (paths.length <= max) return []; + + const resourcesTypes = new Set(paths.map(extractResourceTypeFromPath)); + if (resourcesTypes.size <= max) return []; + + return [ + { + message: `More than ${max} resource types found`, + }, + ]; +}; diff --git a/api-linter/rulesets/functions/get-resources-has-pagination-strategies-meta.js b/api-linter/rulesets/functions/get-resources-has-pagination-strategies-meta.js new file mode 100644 index 0000000..cadd047 --- /dev/null +++ b/api-linter/rulesets/functions/get-resources-has-pagination-strategies-meta.js @@ -0,0 +1,98 @@ +const jp = require('jsonpath') + +const isCollectionResponse = (schema) => { + var props = jp.query(schema, '$.properties.data'); + if (props.length>0){ + if ( props[0].type == 'array') { + return true; + } + } + return false; +}; + +const hasTopLink = (schema) => { + var props = jp.query(schema, '$.properties.links'); + if (props.length>0){ + return true; + } + return false; +}; + +const pageLinkKeys = ["first", "last", "prev", "next"]; + +const hasPageLinkKeys = (prob) => { + var has = false; + pageLinkKeys.forEach(key => { + if(key in prob) { + has = true; + } + }); + return has; +} + +const followCursorBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'cursor' || param.name == 'limit').length == 2; +} + +const followOffsetBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'offset' || param.name == 'limit').length == 2; +} + +const followIndexBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'number' || param.name == 'size').length == 2; +} + +const checkCursorMeta = (meta) => { + return jp.query(meta, '$.properties.page.properties.nextCursor').length > 0 +} + +const checkOffsetMeta = (meta) => { + return jp.query(meta, '$.properties.page.properties.totalElements').length > 0 + && jp.query(meta, '$.properties.page.properties.offset').length > 0 + && jp.query(meta, '$.properties.page.properties.elements').length > 0 +} + +const checkIndexMeta = (meta) => { + return jp.query(meta, '$.properties.page.properties.totalPages').length > 0 + && jp.query(meta, '$.properties.page.properties.number').length > 0 + && jp.query(meta, '$.properties.page.properties.size').length > 0 +} + +const checkIndexMeta2 = (meta) => { + return jp.query(meta, '$.properties.page.properties.elements').length > 0 + && jp.query(meta, '$.properties.page.properties.totalElements').length > 0 +} + +export default (targetValue, options) => { + var responseSchema = jp.query(targetValue, '$.responses..schema'); + if (responseSchema.length > 0){ + var schema = responseSchema[0]; + if (isCollectionResponse(schema) && hasTopLink(schema)){ + var linkProps = jp.query(schema, '$.properties.links.properties'); + if (linkProps.filter(prob => hasPageLinkKeys(prob)).length > 0 ){ + var requestQueryParameters = jp.query(targetValue, "$.parameters[?(@.in=='query')]"); + var responseMeta = jp.query(schema, "$.properties.meta"); + if (options == 'cursor' && requestQueryParameters.length > 0 && followCursorBasedStrategies(requestQueryParameters)){ + if (responseMeta.length == 0 || !checkCursorMeta(responseMeta[0])){ + return [{message: 'The server MAY provide information about the nextCursor that is required to fetch the next page in the meta.page.nextCursor property'}]; + } + } + if (options == 'offset' && requestQueryParameters.length > 0 && followOffsetBasedStrategies(requestQueryParameters)){ + if (responseMeta.length == 0 || !checkOffsetMeta(responseMeta[0])){ + return [{message: 'Server SHOULD provide pagination meta [meta.page.totalElements, meta.page.offset, meta.page.elements] information to the client'}]; + } + } + if (options == 'index1' && requestQueryParameters.length > 0 && followIndexBasedStrategies(requestQueryParameters)){ + if (responseMeta.length == 0 || !checkIndexMeta(responseMeta[0])){ + return [{message: 'Server SHOULD provide pagination meta [meta.page.totalPages, meta.page.number, meta.page.size] information to the client'}]; + } + } + if (options == 'index2' && requestQueryParameters.length > 0 && followIndexBasedStrategies(requestQueryParameters)){ + if (responseMeta.length == 0 || (checkIndexMeta(responseMeta[0]) && !checkIndexMeta2(responseMeta[0]))) { + return [{message: 'Server SHOULD provide pagination meta [meta.page.elements, meta.page.totalElements] information to the client'}]; + } + } + } + } + } +}; \ No newline at end of file diff --git a/api-linter/rulesets/functions/get-resources-has-pagination-strategies.js b/api-linter/rulesets/functions/get-resources-has-pagination-strategies.js new file mode 100644 index 0000000..76a54a0 --- /dev/null +++ b/api-linter/rulesets/functions/get-resources-has-pagination-strategies.js @@ -0,0 +1,60 @@ +const jp = require('jsonpath') + +const isCollectionResponse = (schema) => { + var props = jp.query(schema, '$.properties.data'); + if (props.length>0){ + if ( props[0].type == 'array') { + return true; + } + } + return false; +}; + +const hasTopLink = (schema) => { + var props = jp.query(schema, '$.properties.links'); + if (props.length>0){ + return true; + } + return false; +}; + +const pageLinkKeys = ["first", "last", "prev", "next"]; + +const hasPageLinkKeys = (prob) => { + var has = false; + pageLinkKeys.forEach(key => { + if(key in prob) { + has = true; + } + }); + return has; +} + +const followCursorBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'cursor' || param.name == 'limit').length == 2; +} + +const followOffsetBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'offset' || param.name == 'limit').length == 2; +} + +const followIndexBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'number' || param.name == 'size').length == 2; +} + +export default (targetValue) => { + var responseSchema = jp.query(targetValue, '$.responses..schema'); + if (responseSchema.length > 0){ + var schema = responseSchema[0]; + if (isCollectionResponse(schema) && hasTopLink(schema)){ + var linkProps = jp.query(schema, '$.properties.links.properties'); + if (linkProps.filter(prob => hasPageLinkKeys(prob)).length > 0 ){ + var requestQueryParameters = jp.query(targetValue, "$.parameters[?(@.in=='query')]"); + if (requestQueryParameters.length == 0 || !(followCursorBasedStrategies(requestQueryParameters) + || followOffsetBasedStrategies(requestQueryParameters) || followIndexBasedStrategies(requestQueryParameters))){ + return [{message: 'Pagination SHOULD be implemented using query parameters.'}]; + } + } + } + } +}; \ No newline at end of file diff --git a/api-linter/rulesets/functions/get-resources-has-pagination.js b/api-linter/rulesets/functions/get-resources-has-pagination.js new file mode 100644 index 0000000..10369ff --- /dev/null +++ b/api-linter/rulesets/functions/get-resources-has-pagination.js @@ -0,0 +1,94 @@ +const jp = require('jsonpath') + +const isCollectionResponse = (schema) => { + var props = jp.query(schema, '$.properties.data'); + if (props.length>0){ + if ( props[0].type == 'array') { + return true; + } + } + return false; +}; + +const hasTopLink = (schema) => { + var props = jp.query(schema, '$.properties.links'); + if (props.length>0){ + return true; + } + return false; +}; + +const pageLinkKeys = ["first", "last", "prev", "next", "self"]; + +const hasPageLinkKeys = (prob) => { + var has = false; + pageLinkKeys.forEach(key => { + if(key in prob) { + has = true; + } + }); + return has; +} + +const notInPageKeys = (prob) => { + const result = Object.keys(prob).filter(key => !pageLinkKeys.includes(key)); + return result.length > 0; +} + +const followCursorBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'cursor' || param.name == 'limit').length == 2; +} + +const followOffsetBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'offset' || param.name == 'limit').length == 2; +} + +const followIndexBasedStrategies = (requestQueryParameters) =>{ + return requestQueryParameters.filter(param => param.name == 'number' || param.name == 'size').length == 2; +} + +export default (targetValue, options) => { + //valid only targetValue using pagination + var requestQueryParameters = jp.query(targetValue, "$.parameters[?(@.in=='query')]"); + if (requestQueryParameters.length > 0 && (followCursorBasedStrategies(requestQueryParameters) + || followOffsetBasedStrategies(requestQueryParameters) || followIndexBasedStrategies(requestQueryParameters))){ + var response = jp.query(targetValue, '$.responses..schema'); + if (response.length > 0){ + var responseSchema = response[0]; + //optionA: A server MAY provide links to traverse a paginated data set + //optionB: Pagination links SHOULD appear in the top-level links object. + //optionC: Pagination links MUST preserve sorting and filtering. + //optionD: Naming convention MUST be used for pagination keys. + if (options == 'A'){ + if (isCollectionResponse(responseSchema) && !hasTopLink(responseSchema)){ + return [{message: 'A server MAY provide links to traverse a paginated data set'}]; + } + else if (isCollectionResponse(responseSchema) && hasTopLink(responseSchema)){ + var linkProps = jp.query(responseSchema, '$.properties.links.properties'); + if (linkProps.filter(prob => hasPageLinkKeys(prob)).length == 0 ){ + return [{message: 'A server MAY provide links to traverse a paginated data set'}]; + } + } + } + if (options == 'B') { + if (isCollectionResponse(responseSchema) && hasTopLink(responseSchema)){ + var linkProps = jp.query(responseSchema, '$.properties.links.properties..properties'); + if (linkProps.filter(prob => hasPageLinkKeys(prob)).length > 0 ){ + return [{message: 'Pagination links SHOULD appear in the top-level links object.'}]; + } + } + } + if (options == 'D') { + if (isCollectionResponse(responseSchema) && hasTopLink(responseSchema)){ + var linkProps = jp.query(responseSchema, '$.properties.links.properties'); + if (linkProps.filter(prob => hasPageLinkKeys(prob)).length > 0 ){ + if (linkProps.filter(prob => notInPageKeys(prob)).length > 0){ + return [{message: 'Naming convention MUST be used for pagination keys.'}]; + } + } + } + } + } + } + +}; \ No newline at end of file diff --git a/api-linter/rulesets/functions/get-resources-has-parameter.js b/api-linter/rulesets/functions/get-resources-has-parameter.js new file mode 100644 index 0000000..9e7a27b --- /dev/null +++ b/api-linter/rulesets/functions/get-resources-has-parameter.js @@ -0,0 +1,19 @@ +const jp = require('jsonpath') + +const isCollectionResponse = (schema) => { + var props = jp.query(schema, '$.responses.*..data'); + if (props.length>0){ + if ( props[0].type == 'array') { + return true; + } + } + return false; +}; + +export default (targetValue, options) => { + if (isCollectionResponse(targetValue)){ + if(targetValue.parameters == null || targetValue.parameters.filter((r) => r.in === 'query' && r.name === options).length == 0){ + return [{message: 'API MAY provide projection of '+ options +' in response'}]; + } + } +}; \ No newline at end of file diff --git a/api-linter/rulesets/functions/get-status-code.js b/api-linter/rulesets/functions/get-status-code.js new file mode 100644 index 0000000..406947c --- /dev/null +++ b/api-linter/rulesets/functions/get-status-code.js @@ -0,0 +1,59 @@ +'use strict'; +const jp = require('jsonpath') + +const checkIsTypeOfOption = (schema, options) => { + var props = jp.query(schema, '$..*..data'); + if (Array.isArray(props) && props.length > 0){ + if (options == 'individual' && ((props[0] != null && props[0].type == 'object') + || (props[0].allOf!=null && props[0].allOf[0].type == 'object'))) { + return true; + } + else if (options == 'collection' && (props[0].type == 'array')) { + return true; + } + return false; + } + else { + if (options == 'individual' & props.type == 'object') { + return true; + } + else if (options == 'collection' && props.type == 'array') { + return true; + } + return false; + } +}; + +const checkOption = (object, options) => { + var schemas = jp.query(object, '$..content..schema'); + var isTypeOfOption = false; + if (Array.isArray(schemas) && schemas.length > 0){ + schemas.filter(schema => typeof schema === 'object' && schema !== null).forEach(schema => { + isTypeOfOption = isTypeOfOption || checkIsTypeOfOption(schema, options); + }) + return isTypeOfOption; + } + return true; +}; + +export default (targetValue, options) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + const results = []; + if (options=='individual' && checkOption(targetValue, options)){ + if(typeof targetValue['200'] == 'undefined'){ + results.push({message: "A Successful fetch of individual resources MUST return the status code 200"}) + } + } + else if(options=='collection' && checkOption(targetValue, options)){ + var exist = false; + for(var key in targetValue){ + if (key.match("^2\\d\\d$")){ + exist = true; + } + } + if (!exist){ + results.push({message: "A successful fetch of resource collection request MUST return a 2xx status code"}) + } + } + return results; +}; diff --git a/api-linter/rulesets/functions/has-header-parameter-with-property-value.js b/api-linter/rulesets/functions/has-header-parameter-with-property-value.js new file mode 100644 index 0000000..c844633 --- /dev/null +++ b/api-linter/rulesets/functions/has-header-parameter-with-property-value.js @@ -0,0 +1,21 @@ +export default (targetVal, options) => { + if (typeof targetVal !== 'object' || targetVal == null) return; + const results = []; + if (targetVal.parameters == null){ + results.push({message: 'No parameters provided.'}) + return results; + } + var params = targetVal.parameters; + var exist = false; + for (const param of params){ + for (const ops of options){ + if (param[ops['property']] === ops['value'] && param['in'] === "header"){ + exist = true; + } + } + } + if (!exist){ + results.push({message: 'No header parameter provided in the options'}) + } + return results; +}; \ No newline at end of file diff --git a/api-linter/rulesets/functions/has-headers-with-property.js b/api-linter/rulesets/functions/has-headers-with-property.js new file mode 100644 index 0000000..6f89dbf --- /dev/null +++ b/api-linter/rulesets/functions/has-headers-with-property.js @@ -0,0 +1,11 @@ +export default (targetVal, options) => { + if (typeof targetVal !== 'object' || targetVal == null) return; + const results = []; + if (typeof targetVal["headers"] == 'undefined'){ + results.push({message: "Response should have headers for Api-Version"}) + } + else if (typeof targetVal["headers"][options['property']] == 'undefined'){ + results.push({message: "A header with full semantic version value MUST be returned in the response with 'Api-Version: ..'"}) + } + return results; +}; \ No newline at end of file diff --git a/api-linter/rulesets/functions/is-meta-object.js b/api-linter/rulesets/functions/is-meta-object.js new file mode 100644 index 0000000..39f5009 --- /dev/null +++ b/api-linter/rulesets/functions/is-meta-object.js @@ -0,0 +1,29 @@ +'use strict'; + +const assertObjectSchema = (schema) => { + if (schema.type !== 'object') { + throw 'Schema type is not `object`'; + } +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.filter(s => typeof s === 'object' && s !== null).forEach(check); + } else { + assertObjectSchema(schema); + } +}; + +export default (targetValue) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/is-object-schema.js b/api-linter/rulesets/functions/is-object-schema.js new file mode 100644 index 0000000..145d307 --- /dev/null +++ b/api-linter/rulesets/functions/is-object-schema.js @@ -0,0 +1,29 @@ +'use strict'; + +const assertObjectSchema = (schema) => { + if (schema.type !== 'object' && !schema.$ref) { + throw 'Schema type is not `object`'; + } +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.filter(s => typeof s === 'object' && s !== null).forEach(check); + } else { + assertObjectSchema(schema); + } +}; + +export default (targetValue) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/is-problem-json-schema.js b/api-linter/rulesets/functions/is-problem-json-schema.js new file mode 100644 index 0000000..8f7fdd1 --- /dev/null +++ b/api-linter/rulesets/functions/is-problem-json-schema.js @@ -0,0 +1,68 @@ +'use strict'; + +/* +Minimal required problem json schema: + +type: object +properties: + type: + type: string + format: uri + title: + type: string + status: + type: integer + format: int32 + detail: + type: string + instance: + type: string +*/ + +const assertProblemSchema = (schema) => { + if (schema.type !== 'object') { + throw "Problem json must have type 'object'"; + } + const type = (schema.properties || {}).type || {}; + if (type.type !== 'string' || type.format !== 'uri') { + throw "Problem json must have property 'type' with type 'string' and format 'uri'"; + } + const title = (schema.properties || {}).title || {}; + if (title.type !== 'string') { + throw "Problem json must have property 'title' with type 'string'"; + } + const status = (schema.properties || {}).status || {}; + if (status.type !== 'integer' || status.format !== 'int32') { + throw "Problem json must have property 'status' with type 'integer' and format 'int32'"; + } + const detail = (schema.properties || {}).detail || {}; + if (detail.type !== 'string') { + throw "Problem json must have property 'detail' with type 'string'"; + } + const instance = (schema.properties || {}).instance || {}; + if (instance.type !== 'string') { + throw "Problem json must have property 'instance' with type 'string'"; + } +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.filter(s => typeof s === 'object' && s !== null).forEach(check); + } else { + assertProblemSchema(schema); + } +}; + +export default (targetValue) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/links-has-self.js b/api-linter/rulesets/functions/links-has-self.js new file mode 100644 index 0000000..c777fb4 --- /dev/null +++ b/api-linter/rulesets/functions/links-has-self.js @@ -0,0 +1,42 @@ +'use strict'; +const jp = require('jsonpath') + +const checkSelf = (link) => { + if (link["self"] == null){ + throw 'self property not found in links object'; + } +} + +const assertLinkSchema = (schema) => { + const paths = jp.paths(schema, '$..*..links.properties'); + + for (const path of paths) { + if (path.some(p => p === 'relationships')) { + continue; + } + const link = jp.value(schema, jp.stringify(path)); + checkSelf(link); + } +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.filter(s => typeof s === 'object' && s !== null).forEach(check); + } else { + assertLinkSchema(schema); + } +}; + +export default (targetValue) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/path-resource-name-is-lowercase-with-hyphen.js b/api-linter/rulesets/functions/path-resource-name-is-lowercase-with-hyphen.js new file mode 100644 index 0000000..3506ad3 --- /dev/null +++ b/api-linter/rulesets/functions/path-resource-name-is-lowercase-with-hyphen.js @@ -0,0 +1,34 @@ +'use strict'; + +const validateResource = (resource) => { + if (! /^([a-z][a-z0-9]*)((-)[a-z0-9]+)*$/ .test(resource)){ + throw "Resource names SHOULD be in lowercase with hyphen"; + } +} + +const check = (path) => { + if (path == null) { + return; + } + var resources = path.split('/'); + for (let index = 0; index < resources.length; index++) { + const element = resources[index]; + if (element.length > 0 && !element.startsWith('{') && !element.endsWith('}')) + { + validateResource(element); + } + } +}; + +export default (targetValue) => { + if (targetValue == null) return; + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/query-parameters-length.js b/api-linter/rulesets/functions/query-parameters-length.js new file mode 100644 index 0000000..965584e --- /dev/null +++ b/api-linter/rulesets/functions/query-parameters-length.js @@ -0,0 +1,18 @@ +export default (targetValue, options) => { + if (targetValue == null) return; + var checkField = options.nameCheck != null; + var queryParameterCount = targetValue.filter((r) => r.in === 'query').length; + var hasFilter = targetValue.filter((r) => r.in === 'query' && r.name === 'filter').length > 0; + if (((queryParameterCount > options.max && !hasFilter) || (hasFilter && queryParameterCount -1 > options.max)) && !checkField){ + return [{message: 'API MAY not support more than '+ options.max + + ' parameters at once for selecting resources. ' + + '(Linter is unable to determine whether the parameter belongs to the filtering condition or not.) ' + }]; + } + if (queryParameterCount > options.max && checkField && !hasFilter){ + return [{message: "The query parameter 'filter' SHOULD be used to filter or query data. " + + "(Linter is unable to determine whether the parameters used for filtering or not.) " + }]; + } +}; + \ No newline at end of file diff --git a/api-linter/rulesets/functions/request-is-defined-document-structure-schema.js b/api-linter/rulesets/functions/request-is-defined-document-structure-schema.js new file mode 100644 index 0000000..1b2adba --- /dev/null +++ b/api-linter/rulesets/functions/request-is-defined-document-structure-schema.js @@ -0,0 +1,37 @@ +'use strict'; + +const assertObjectSchema = (schema) => { + var exist = false; + for(var key in schema.properties) + { + if (key === "data"){ + exist = true; + break; + } + } + if (!exist && !schema.$ref) { + throw 'A request document SHOULD contain at least data property'; + } +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.filter(s => typeof s === 'object' && s !== null).forEach(check); + } else { + assertObjectSchema(schema); + } +}; + +export default (targetValue) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/response-is-defined-document-structure-schema.js b/api-linter/rulesets/functions/response-is-defined-document-structure-schema.js new file mode 100644 index 0000000..e637177 --- /dev/null +++ b/api-linter/rulesets/functions/response-is-defined-document-structure-schema.js @@ -0,0 +1,38 @@ +'use strict'; + +const assertObjectSchema = (schema) => { + var doc_structure = ["data", "errors", "meta", "links"]; + var exist = false; + for(var key in schema.properties) + { + if (doc_structure.includes(key)){ + exist = true; + break; + } + } + if (!exist) { + throw 'A response document SHOULD contain at least one of the following top-level members:data,errors,meta,links.'; + } +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.filter(s => typeof s === 'object' && s !== null).forEach(check); + } else { + assertObjectSchema(schema); + } +}; + +export default (targetValue) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + try { + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/schema-test.js b/api-linter/rulesets/functions/schema-test.js new file mode 100644 index 0000000..22bb820 --- /dev/null +++ b/api-linter/rulesets/functions/schema-test.js @@ -0,0 +1,29 @@ +'use strict'; + +const assertObjectSchema = (schema) => { + console.log("==========================================") + console.log(schema) +}; + +const check = (schema) => { + const combinedSchemas = [...(schema.anyOf || []), ...(schema.oneOf || []), ...(schema.allOf || [])]; + if (combinedSchemas.length > 0) { + combinedSchemas.filter(s => typeof s === 'object' && s !== null).forEach(check); + } else { + assertObjectSchema(schema); + } +}; + +export default (targetValue) => { + if (typeof targetValue !== 'object' || targetValue == null) return; + try { + console.log("*********************************") + check(targetValue); + } catch (ex) { + return [ + { + message: ex, + }, + ]; + } +}; diff --git a/api-linter/rulesets/functions/successful-response-status-code.js b/api-linter/rulesets/functions/successful-response-status-code.js new file mode 100644 index 0000000..83c5d3c --- /dev/null +++ b/api-linter/rulesets/functions/successful-response-status-code.js @@ -0,0 +1,13 @@ +export default (targetValue) => { + const results = []; + var exist = false; + for(var key in targetValue){ + if (key.match("^2\\d\\d$")){ + exist = true; + } + } + if (!exist){ + results.push({message: "API MUST respond successful response with 2xx success status code"}) + } + return results; +}; diff --git a/api-linter/rulesets/functions/validate-b3-tracing.js b/api-linter/rulesets/functions/validate-b3-tracing.js new file mode 100644 index 0000000..4dc9325 --- /dev/null +++ b/api-linter/rulesets/functions/validate-b3-tracing.js @@ -0,0 +1,26 @@ +'use strict'; + +export default (targetValue) => { + if (!Array.isArray(targetValue)) { + return [ + { + message: `No array given, provide $.paths.*.*.parameters`, + }, + ]; + } + + const b3Params = targetValue.filter( + (param) => + param.name && (param.name.toLowerCase() === 'x-b3-traceid' || param.name.toLowerCase() === 'x-b3-spanid'), + ); + + if (b3Params.length !== 2 || !b3Params.every((param) => param.in === 'header')) { + return [ + { + message: `B3 header X-B3-Traceid or X-B3-Spanid missing`, + }, + ]; + } + + return []; +}; diff --git a/api-linter/rulesets/semantic-versioning.yml b/api-linter/rulesets/semantic-versioning.yml new file mode 100644 index 0000000..ab2ea98 --- /dev/null +++ b/api-linter/rulesets/semantic-versioning.yml @@ -0,0 +1,14 @@ +rules: + + Semantic-Versioning-[2.0.0]: + message: '{{error}}' + description: Semantic versioning MUST be used to version individual APIs + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/versioning.html#api-version-numbering + severity: error + given: $.info.version + then: + function: schema + functionOptions: + schema: + type: string + match: '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$' \ No newline at end of file diff --git a/api-linter/rulesets/siemens-api-common-operation.yml b/api-linter/rulesets/siemens-api-common-operation.yml new file mode 100644 index 0000000..8b0c78e --- /dev/null +++ b/api-linter/rulesets/siemens-api-common-operation.yml @@ -0,0 +1,115 @@ +functions: + - is-object-schema + - get-status-code + - links-has-self + - successful-response-status-code + - has-header-parameter-with-property-value + +rules: + + Siemens-API-[800.1]: + message: A Successful fetch of individual resources MUST return the status code 200 + description: A server MUST respond to a successful request to fetch an individual resource or resource collection with a 200 status code response. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops800.1 + severity: error + given: $.paths.*.get.responses + then: + function: get-status-code + functionOptions: individual + + Siemens-API-[800.2]: + message: A successful fetch of resource collection request MUST return a 2xx status code + description: A successful fetch of resource collection request MUST return a 2xx status code. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops800.2 + severity: error + given: $.paths.*.get.responses + then: + function: get-status-code + functionOptions: collection + + Siemens-API-[800.4]: + message: '{{error}}' + description: An API Provider MUST support fetching resource data for provided links. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops800.4 + severity: error + given: $.paths..get.responses..content..schema + then: + function: links-has-self + + Siemens-API-[801]: + message: The POST request MUST include a single resource object as primary data + description: The POST request MUST include a single resource object as primary data + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops801 + severity: error + given: $.paths..post.requestBody..content..schema + then: + function: is-object-schema + + Siemens-API-[801.4]: + message: API MUST respond to successful POST creation request with 2xx success status code + description: API MUST respond to successful POST creation request with 2xx success status code + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops801.4 + severity: error + given: $.paths.*.post.responses + then: + function: successful-response-status-code + + Siemens-API-[802.3]: + message: A PATCH request SHOULD address a single resource only + description: A PATCH request SHOULD address a single resource only + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops802.3 + severity: warn + given: $.paths.*.patch.requestBody..content..schema + then: + function: is-object-schema + + Siemens-API-[802.6]: + message: API MUST respond to successful PATCH update request with 2xx success status code + description: API MUST respond to successful PATCH update request with 2xx success status code + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops802.6 + severity: error + given: $.paths.*.patch.responses + then: + function: successful-response-status-code + + Siemens-API-[803.1]: + message: PUT update request SHOULD be provided only for single resource objects + description: PUT update request SHOULD be provided only for single resource objects + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops803.1 + severity: warn + given: $.paths.*.put.requestBody..content..schema + then: + function: is-object-schema + + Siemens-API-[803.2]: + message: API MUST respond to successful PUT update request with 2xx success status code + description: API MUST respond to successful PUT update request with 2xx success status code + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops803.2 + severity: error + given: $.paths.*.put.responses + then: + function: successful-response-status-code + + Siemens-API-[803.6]: + message: The API consumer MAY use conditional header information to solve concurrent update requests + description: To prevent unnoticed concurrent updates when using PUT, the server endpoint MAY consider to support ETag together with If-Match/If-None-Match header + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops803.6 + severity: info + given: $.paths.*.put + then: + function: has-header-parameter-with-property-value + functionOptions: + - property: name + value: If-Match + - property: name + value: If-None-Match + + Siemens-API-[804.1]: + message: MUST respond to successful DELETE request with 2xx success status code + description: MUST respond to successful DELETE request with 2xx success status code + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/common-operations.html#commonops804.1 + severity: error + given: $.paths.*.delete.responses + then: + function: successful-response-status-code + diff --git a/api-linter/rulesets/siemens-api-error-reporting.yml b/api-linter/rulesets/siemens-api-error-reporting.yml new file mode 100644 index 0000000..d4dfb8a --- /dev/null +++ b/api-linter/rulesets/siemens-api-error-reporting.yml @@ -0,0 +1,187 @@ +rules: + + Siemens-API-[300]: + message: '{{property}} is not using an official response code' + description: API MUST use official HTTP status codes as intended. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/error.html#errorreportingrule300 + severity: error + given: $.paths.*.*.responses.*~ + then: + function: enumeration + functionOptions: + values: + - '100' + - '101' + - '102' + - '103' + - '200' + - '201' + - '202' + - '203' + - '204' + - '205' + - '206' + - '207' + - '208' + - '226' + - '300' + - '301' + - '302' + - '303' + - '304' + - '305' + - '306' + - '307' + - '308' + - '400' + - '401' + - '402' + - '403' + - '404' + - '405' + - '406' + - '407' + - '408' + - '409' + - '410' + - '411' + - '412' + - '413' + - '414' + - '415' + - '416' + - '417' + - '418' + - '421' + - '422' + - '423' + - '424' + - '425' + - '426' + - '427' + - '428' + - '429' + - '430' + - '431' + - '451' + - '500' + - '501' + - '502' + - '503' + - '504' + - '505' + - '506' + - '507' + - '508' + - '509' + - '510' + - '511' + - default + + Siemens-API-[301]: + message: '{{property}} is not a standardized response code' + description: SHOULD only use most common HTTP status codes + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/error.html#errorreportingrule301 + severity: warn + given: $.paths.*.*.responses.*~ + then: + function: enumeration + functionOptions: + values: + - '200' + - '201' + - '202' + - '204' + - '301' + - '303' + - '304' + - '305' + - '307' + - '400' + - '401' + - '403' + - '404' + - '405' + - '406' + - '408' + - '409' + - '412' + - '415' + - '429' + - '500' + - '501' + - '503' + - '504' + - default + + Siemens-API-[302]: + message: '{{property}} is not using most specific HTTP status code for error' + description: Xcelerator API SHOULD use the most specific HTTP status code when returning information about the request processing state or reported error situations + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/error.html#errorreportingrule302 + severity: warn + given: $.components.examples..*.errors.*.status + then: + function: enumeration + functionOptions: + values: + - '400' + - '401' + - '403' + - '404' + - '405' + - '406' + - '408' + - '409' + - '412' + - '415' + - '429' + - '500' + - '501' + - '503' + + Siemens-API-[305]-1: + message: Error object SHOULD be represented according to the defined structure fields + description: Error object SHOULD be represented according to the defined structure fields + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/error.html#errorreportingrule305 + severity: warn + given: $.components.schemas.Errors..*..errors.*.properties.*~ + then: + function: enumeration + functionOptions: + values: + - 'id' + - 'code' + - 'status' + - 'title' + - 'detail' + - 'links' + - 'correlationId' + - 'source' + + Siemens-API-[305]-2: + message: 'The error links resource object MAY contain the members: about, type' + description: 'The error links resource object MAY contain the members: about, type' + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/error.html#errorreportingrule305 + severity: info + given: $.components.schemas.Errors..*..links.properties.*~ + then: + function: enumeration + functionOptions: + values: + - 'about' + - 'type' + + Siemens-API-[305]-3: + message: 'The source object SHOULD include one of the members or be omitted: pointer, parameter, header' + description: 'The source object SHOULD include one of the members or be omitted: pointer, parameter, header' + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/error.html#errorreportingrule305 + severity: warn + given: $.components.schemas.Errors..*..source.properties.*~ + then: + function: enumeration + functionOptions: + values: + - 'pointer' + - 'parameter' + - 'header' + diff --git a/api-linter/rulesets/siemens-api-express.yml b/api-linter/rulesets/siemens-api-express.yml new file mode 100644 index 0000000..4c30b41 --- /dev/null +++ b/api-linter/rulesets/siemens-api-express.yml @@ -0,0 +1,10 @@ +extends: + - ./siemens-api-media-type.yml + - ./siemens-api-error-reporting.yml + - ./siemens-api-filtering.yml + - ./siemens-api-sparse-fieldsets.yml + - ./siemens-api-pagination.yml + - ./siemens-api-sorting.yml + - ./siemens-api-common-operation.yml + - ./siemens-api-versioning.yml + - ./siemens-api-security.yml \ No newline at end of file diff --git a/api-linter/rulesets/siemens-api-filtering.yml b/api-linter/rulesets/siemens-api-filtering.yml new file mode 100644 index 0000000..e647bbf --- /dev/null +++ b/api-linter/rulesets/siemens-api-filtering.yml @@ -0,0 +1,29 @@ +functions: + - query-parameters-length + +rules: + + Siemens-API-[400]: + message: '{{error}}' + description: An API provider SHOULD NOT support more than 4 parameters at once for selecting resources + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/filtering.html#queryingrule400 + severity: hint + given: $.paths..get.parameters + then: + function: query-parameters-length + functionOptions: + max: 4 + + Siemens-API-[401]: + message: '{{error}}' + description: The query parameter filter SHOULD be used to filter or query data + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/filtering.html#queryingrule401 + severity: hint + given: $.paths..get.parameters + then: + function: query-parameters-length + functionOptions: + max: 4 + nameCheck: filter + + diff --git a/api-linter/rulesets/siemens-api-media-type.yml b/api-linter/rulesets/siemens-api-media-type.yml new file mode 100644 index 0000000..552fb99 --- /dev/null +++ b/api-linter/rulesets/siemens-api-media-type.yml @@ -0,0 +1,118 @@ +functions: + - is-object-schema + - is-meta-object + - assert-http-codes-for-operation + - count-resource-types + - is-problem-json-schema + - response-is-defined-document-structure-schema + - request-is-defined-document-structure-schema + - path-resource-name-is-lowercase-with-hyphen + +rules: + + Siemens-API-[100]: + message: SHOULD use JSON-based media types + description: Siemens Xcelerator recommends use either the media type application/json for exchanging data, or a more specific media type based on JSON. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule100 + severity: info + given: $.paths.*.*.responses.*.content.*~ + then: + function: pattern + functionOptions: + match: ^application\/(json|.*?\+json)$ + + Siemens-API-[101.1]: + message: 'SHOULD use a top-level JSON object' + description: The root of every document SHOULD contain a JSON object. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.1 + severity: warn + given: $.paths.*.*[responses,requestBody]..content..schema + then: + function: is-object-schema + + Siemens-API-[101.2]-1: + message: 'SHOULD use the defined document structure' + description: A response document SHOULD contain at least one of the following top-level members:data,errors,meta,links. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.2 + severity: warn + given: $.paths.*.*[responses]..content..schema + then: + function: response-is-defined-document-structure-schema + + Siemens-API-[101.2]-2: + message: 'SHOULD use the defined document structure' + description: A request document SHOULD contain at least data. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.2 + severity: warn + given: $.paths.*.*[requestBody]..content..schema + then: + function: request-is-defined-document-structure-schema + + Siemens-API-[101.4.1]: + message: The data type MUST be compliant with the types defined + description: The data type MUST be compliant with the types defined in the OpenAPI specification + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.4.1 + severity: error + given: $.paths.*.*[responses,requestBody]..content..schema..properties..*..type + then: + function: pattern + functionOptions: + match: ^(boolean|object|array|integer|number|string)$ + + Siemens-API-[101.5]: + message: A links object SHOULD contain one or more links + description: A links object SHOULD contain one or more links + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.5 + severity: warn + given: $.*..schema..links + then: + function: is-object-schema + + Siemens-API-[101.7]: + message: A JSON document MAY provide meta information. + description: Meta information can be used to include additional information to a JSON object. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.7 + severity: info + given: $.*..schema..meta + then: + function: is-object-schema + + Siemens-API-[101.7.2]: + message: A meta object SHOULD be used to represent meta information as a JSON object. + description: The value of each meta field MUST be a JSON object + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.7.2 + severity: error + given: $.*..schema..meta + then: + function: is-meta-object + + Siemens-API-[101.8]: + message: Field names SHOULD use lower camel case + description: Field names SHOULD use lower camel case + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.8 + severity: warn + given: $.paths.*.*[responses,requestBody]..content..schema..properties.*~ + then: + function: casing + functionOptions: + type: camel + + Siemens-API-[101.8.1]: + message: Field names MUST use the allowed characters only + description: Field names MUST use the allowed characters only + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/media-type.html#mediatyperule101.8.1 + severity: error + given: $.paths.*.*[responses,requestBody]..content..schema..properties.*~ + then: + function: pattern + functionOptions: + match: ^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$ + + Siemens-API-[101.9]: + message: Resource names SHOULD be in lowercase with hyphen + description: Resource names in an URL path (including resource names) SHOULD be lower case words separated by hyphens + severity: warn + given: $.paths.*~ + then: + function: path-resource-name-is-lowercase-with-hyphen + diff --git a/api-linter/rulesets/siemens-api-pagination.yml b/api-linter/rulesets/siemens-api-pagination.yml new file mode 100644 index 0000000..11b9ed5 --- /dev/null +++ b/api-linter/rulesets/siemens-api-pagination.yml @@ -0,0 +1,85 @@ +functions: + - get-resources-has-pagination + - get-resources-has-pagination-strategies + - get-resources-has-pagination-strategies-meta + +rules: + + Siemens-API-[600]-1: + message: '{{error}}' + description: A server MAY provide links to traverse a paginated data set + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule600 + severity: info + given: $.paths..get + then: + function: get-resources-has-pagination + functionOptions: A + + Siemens-API-[600]-2: + message: '{{error}}' + description: Pagination links SHOULD appear in the top-level links object. + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule600 + severity: warn + given: $.paths..get + then: + function: get-resources-has-pagination + functionOptions: B + + Siemens-API-[600]-3: + message: '{{error}}' + description: Naming convention MUST be used for pagination keys + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule600 + severity: hint + given: $.paths..get + then: + function: get-resources-has-pagination + functionOptions: D + + Siemens-API-[601]: + message: '{{error}}' + description: Pagination SHOULD be implemented using query parameters + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule601 + severity: warn + given: $.paths..get + then: + function: get-resources-has-pagination-strategies + + Siemens-API-[601.1.1]: + message: '{{error}}' + description: Server MAY provide pagination meta information to the client + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule601.1.1 + severity: info + given: $.paths..get + then: + function: get-resources-has-pagination-strategies-meta + functionOptions: cursor + + Siemens-API-[601.2.1]: + message: '{{error}}' + description: Server MAY provide pagination meta information to the client + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule601.2.1 + severity: info + given: $.paths..get + then: + function: get-resources-has-pagination-strategies-meta + functionOptions: offset + + Siemens-API-[601.3.1]-1: + message: '{{error}}' + description: Server MAY provide pagination meta information to the client + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule601.3.1 + severity: info + given: $.paths..get + then: + function: get-resources-has-pagination-strategies-meta + functionOptions: index1 + + Siemens-API-[601.3.1]-2: + message: '{{error}}' + description: Server MAY provide pagination meta information to the client + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/pagination.html#paginationrule601.3.1 + severity: info + given: $.paths..get + then: + function: get-resources-has-pagination-strategies-meta + functionOptions: index2 \ No newline at end of file diff --git a/api-linter/rulesets/siemens-api-security.js b/api-linter/rulesets/siemens-api-security.js new file mode 100644 index 0000000..45cecb7 --- /dev/null +++ b/api-linter/rulesets/siemens-api-security.js @@ -0,0 +1,57 @@ +var apiSecurity = global.apiSecurity; +const hasHeaderWithPropertyValue = (targetVal, options) => { + const results = []; + if (targetVal.parameters == null){ + results.push({message: 'No request header parameters provided.'}) + return results; + } + var exist = false; + for (const param of targetVal.parameters){ + if (param[options['property']] === options['value'] && param['in'] === "header"){ + exist = true; + } + } + if (!exist){ + results.push({message: 'A request header '+ options['value'] +' MUST be supported.'}) + } + return results; +}; + +var ruleset = {}; +if (typeof apiSecurity == 'undefined') { + apiSecurity = "n"; +} +switch (apiSecurity.toLowerCase()){ + case "y": + case "t": + case "true": + case "yes": + ruleset.rules = { + "Siemens-API-Security": { + message: "{{error}}", + description: "When calling a secured REST API, the request header Authorization with the value Bearer MUST be present.", + documentationUrl: "https://developer.siemens.com/guidelines/api-guidelines/rest/security.html", + severity: "error", + given: [ + "$.paths.*" + ], + then: { + function: hasHeaderWithPropertyValue, + functionOptions: { + property: "name", + value: "Authorization" + } + } + } + } + break; + case "ignore": + case "n": + case "no": + case "f": + case "false": + default: + ruleset.rules = {} + break; +} +export default ruleset; \ No newline at end of file diff --git a/api-linter/rulesets/siemens-api-security.yml b/api-linter/rulesets/siemens-api-security.yml new file mode 100644 index 0000000..32b73f7 --- /dev/null +++ b/api-linter/rulesets/siemens-api-security.yml @@ -0,0 +1,2 @@ +extends: + - ./siemens-api-security.js diff --git a/api-linter/rulesets/siemens-api-sorting.yml b/api-linter/rulesets/siemens-api-sorting.yml new file mode 100644 index 0000000..67cd771 --- /dev/null +++ b/api-linter/rulesets/siemens-api-sorting.yml @@ -0,0 +1,15 @@ +functions: + - get-resources-has-parameter + +rules: + + Siemens-API-[700]: + message: '{{error}}' + description: API MAY provide projection of sort in response + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/sorting.html#sortingrule700 + severity: info + given: $.paths..get + then: + function: get-resources-has-parameter + functionOptions: sort + \ No newline at end of file diff --git a/api-linter/rulesets/siemens-api-sparse-fieldsets.yml b/api-linter/rulesets/siemens-api-sparse-fieldsets.yml new file mode 100644 index 0000000..784835a --- /dev/null +++ b/api-linter/rulesets/siemens-api-sparse-fieldsets.yml @@ -0,0 +1,16 @@ +functions: + - get-resources-has-parameter + +rules: + + Siemens-API-[500]: + message: '{{error}}' + description: API MAY provide projection of fieldset in response + documentationUrl: https://developer.siemens.com/guidelines/api-guidelines/rest/sparse-fieldsets.html#sparsefieldsetrule500 + severity: info + given: $.paths..get + then: + function: get-resources-has-parameter + functionOptions: fields + + diff --git a/api-linter/rulesets/siemens-api-versioning.js b/api-linter/rulesets/siemens-api-versioning.js new file mode 100644 index 0000000..128917a --- /dev/null +++ b/api-linter/rulesets/siemens-api-versioning.js @@ -0,0 +1,94 @@ +const {pattern} = require('@stoplight/spectral-functions') + +const apiVersioning = global.apiVersioning; +const hasPropertyValue = (targetVal, options) => { + const results = []; + var exist = false; + for (const param of targetVal){ + if (param[options['property']] === options['value']){ + exist = true; + } + } + if (!exist){ + results.push({message: 'A request header Api-Version MUST be supported to allow client to specify the version.'}) + } + return results; +}; + +const hasProperty = (targetVal, options) => { + const results = []; + if (typeof targetVal["headers"] == 'undefined'){ + results.push({message: "Response should have headers for Api-Version"}) + } + else if (typeof targetVal["headers"][options['property']] == 'undefined'){ + results.push({message: "A header with full semantic version value MUST be returned in the response with 'Api-Version: ..'"}) + } + return results; +}; + +var ruleset = {}; +switch (apiVersioning){ + case "ignore": + ruleset.rules = {} + break; + case "url": + ruleset.rules = { + "Siemens-API-[200.1]": { + message: "The Major version number MUST be specified in the URI as a path segment", + description: "The Major version number MUST be specified in the URI as a path segment.", + documentationUrl: "https://developer.siemens.com/guidelines/api-guidelines/rest/versioning.html#versioningrule200.1", + severity: "error", + given: [ + "$.servers..url" + ], + then: { + function: pattern, + functionOptions: { + match: "/[\\.|\\/|](v)?[0-9]+/i", + notMatch: "/[\\.|\\/|](v)?[0-9]+\\.[0-9]+/i" + } + } + } + } + break; + case "header": + ruleset.rules = { + "Siemens-API-[200.2]": { + message: "{{message}}", + description: "A request HTTP header e.g., Api-Version Should be supported to allow client to specify the version.", + documentationUrl: "https://developer.siemens.com/guidelines/api-guidelines/rest/versioning.html#versioningrule200.2", + severity: "error", + given: [ + "$.paths[*]..parameters" + ], + then: { + function: hasPropertyValue, + functionOptions: { + property: "name", + value: "Api-Version" + } + } + } + } + break; + default: + break; +} +if (apiVersioning == 'url' || apiVersioning == 'header'){ + ruleset.rules["Siemens-API-[201]"] = { + message: "{{message}}", + description: "A header with full semantic version value SHOULD be returned in the response with 'Api-Version: ..'", + documentationUrl: "https://developer.siemens.com/guidelines/api-guidelines/rest/versioning.html#versioningrule201", + severity: "warn", + given: [ + "$.paths..responses[?(@.content)]" + ], + then: { + function: hasProperty, + functionOptions: { + property: "Api-Version" + } + } + } +} +export default ruleset; \ No newline at end of file diff --git a/api-linter/rulesets/siemens-api-versioning.yml b/api-linter/rulesets/siemens-api-versioning.yml new file mode 100644 index 0000000..3190e98 --- /dev/null +++ b/api-linter/rulesets/siemens-api-versioning.yml @@ -0,0 +1,3 @@ +extends: + - ./semantic-versioning.yml + - ./siemens-api-versioning.js diff --git a/api-linter/rulesets/siemens-api.yml b/api-linter/rulesets/siemens-api.yml new file mode 100644 index 0000000..8f0948e --- /dev/null +++ b/api-linter/rulesets/siemens-api.yml @@ -0,0 +1,11 @@ +extends: + - ./siemens-api-media-type.yml + - ./siemens-api-versioning.yml + - ./siemens-api-error-reporting.yml + - ./siemens-api-filtering.yml + - ./siemens-api-sparse-fieldsets.yml + - ./siemens-api-pagination.yml + - ./siemens-api-sorting.yml + - ./siemens-api-common-operation.yml + - ./siemens-api-security.yml + - "spectral:oas" \ No newline at end of file diff --git a/api-linter/rulesets/vs-extension.yml b/api-linter/rulesets/vs-extension.yml new file mode 100644 index 0000000..eace2ca --- /dev/null +++ b/api-linter/rulesets/vs-extension.yml @@ -0,0 +1,10 @@ +extends: + - ./siemens-api-media-type.yml + - ./siemens-api-error-reporting.yml + - ./siemens-api-filtering.yml + - ./siemens-api-sparse-fieldsets.yml + - ./siemens-api-pagination.yml + - ./siemens-api-sorting.yml + - ./siemens-api-common-operation.yml + - ./semantic-versioning.yml + - "spectral:oas" \ No newline at end of file diff --git a/api-linter/src/argument.js b/api-linter/src/argument.js new file mode 100644 index 0000000..1e7a012 --- /dev/null +++ b/api-linter/src/argument.js @@ -0,0 +1,48 @@ +'use strict'; +const { Command, Option } = require("commander"); + +exports.argument = () => { + const program = new Command(); + + program.name("api-linter") + .requiredOption("-s, --specPath ", "path to openapi specification") + .requiredOption("-r, --rulesetPath ", "path to ruleset file",(value, previous) => { + if (!Array.isArray(previous)) { + return [value]; + } + previous.push(value); + return previous; + }, + [] + ) + .addOption(new Option("-f, --failSeverity ", "fail severity").default("warn")) + .addOption(new Option("-c, --consoleSeverity ", "console output severity").default("warn")) + .addOption(new Option("-o, --outputFilename ", "output filename").default("linter-result.html")) + .addOption(new Option("-p, --jsonFile ", "output json file").default("spectral_result.json")) + .addOption(new Option("-v, --apiVersioning ", "api versioning") + .choices(["ignore", "url", "header"]) + .default("ignore")) + .addOption(new Option("-a, --apiSecurity ", "api security") + .choices(["y","yes","t","true","n","no","f","false"]) + .default("n")) + .option("--resolve", "follow external $refs (same as Spectral --resolve)", false); + + program.parse(process.argv); + + const options = program.opts(); + const { specPath, rulesetPath, failSeverity, consoleSeverity, + apiVersioning, apiSecurity, outputFilename, resolve, jsonFile } = options; + global.apiVersioning = apiVersioning; + global.apiSecurity = apiSecurity; + return ({ + specPath, + rulesetPath, + failSeverity, + consoleSeverity, + apiVersioning, + apiSecurity, + outputFilename, + resolve, + jsonFile + }); +} \ No newline at end of file diff --git a/api-linter/src/extension.js b/api-linter/src/extension.js new file mode 100644 index 0000000..335259d --- /dev/null +++ b/api-linter/src/extension.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const spectralCore = require('@stoplight/spectral-core'); +const Parsers = require('@stoplight/spectral-parsers'); +const spectralRuntime = require('@stoplight/spectral-runtime'); +const {bundleAndLoadRuleset} = require('@stoplight/spectral-ruleset-bundler/with-loader'); +const fetch = spectralRuntime; +const {Spectral, Document} = spectralCore; + +const linter = async (specFilePath, rulesetFilepath) => { + const myDocument = new Document( + fs.readFileSync(specFilePath, "utf-8").trim(), + Parsers.Yaml, + "openapi.yml", + ); + const spectral = new Spectral(); + var rules = await bundleAndLoadRuleset(rulesetFilepath,{ fs, fetch }); + spectral.setRuleset(rules); + var binded = {}; + await spectral.run(myDocument).then(results => { + binded.rules = spectral.ruleset.rules; + binded.results = results; + }); + return binded; +} + +exports.validator = () => { + const validate = async (specFilePath, ruleSetFilePath) => { + var ret = {}; + await linter(specFilePath, ruleSetFilePath).then(linterResult => { + ret = linterResult; + }); + return ret; + }; + return ( + validate + ); +} + + diff --git a/api-linter/src/index.js b/api-linter/src/index.js new file mode 100755 index 0000000..88d8054 --- /dev/null +++ b/api-linter/src/index.js @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const {join} = require('path'); +const process = require('process') +const spectralCore = require('@stoplight/spectral-core'); +const Parsers = require('@stoplight/spectral-parsers'); +const spectralRuntime = require('@stoplight/spectral-runtime'); +const { resolver } = require('@stoplight/spectral-ref-resolver'); +const {DiagnosticSeverity} = require('@stoplight/types'); +const {bundleAndLoadRuleset} = require('@stoplight/spectral-ruleset-bundler/with-loader'); + +const Handlebars = require('handlebars'); +const Underscore = require('underscore'); +const {argument} = require('./argument'); + +const fetch = spectralRuntime; +const {Spectral, Document} = spectralCore; +const {specPath, rulesetPath, failSeverity, consoleSeverity, outputFilename, jsonFile,resolve: resolveRefs } = argument(); +const yaml = require('js-yaml'); + +var reportFileName = outputFilename; +if (!outputFilename.endsWith(".html")){ + reportFileName = reportFileName + ".html"; +} + +const fileExists = (filepath) => { + return fs.existsSync(path.join(filepath)); +} + +const loadFile = (filepath) => { + return fs.readFileSync(path.join(filepath), { encoding: "utf8" }); +} + +const writeToFile = (data, filename) => { + fs.writeFileSync(path.join(filename), data, { encoding: "utf8" }); +} + +const specFilePath = path.resolve(specPath); +if (!fileExists(specFilePath)){ + throw Error(`Unable to resolve spec file path ${specFilePath}`); +} + +for (const filepath of rulesetPath) { + const abs_filepath = path.resolve(filepath) + if (!fileExists(abs_filepath)){ + throw Error(`Unable to resolve ruleset file path ${abs_filepath}`); + } +} + +const formatJSON = (rules, results) => { + const severityMap = getSeverityMap(); + var totalRuleCount = 0; + var enabledCount = 0; + var data = []; + var index = 0; + for (const key in rules){ + var rule = rules[key]; + var ruleResult = {}; + ruleResult.ruleName = rule.name; + ruleResult.ruleDescription = rule.description; + ruleResult.documentUrl = rule.documentationUrl; + ruleResult.details = []; + ruleResult.severity = rule.severity; + ruleResult.hasDetails = false; + if (rule.enabled){ + ruleResult.success = true; + ruleResult.enabled = true; + ruleResult.class = "table-success"; + ruleResult.message = "success"; + ruleResult.index = index; + enabledCount++; + index++; + data.push(ruleResult); + totalRuleCount++; + } + } + var newResults = []; + results.forEach( o => { + let newObj = {}; + newObj.line = (o.range.start.line + 1) + ":" + (o.range.start.character + 1); + newObj.severity = severityMap[o.severity]; + newObj.code = o.code; + newObj.message = o.message; + newObj.path = o.path.join("_"); + newResults.push(newObj); + for (const ruleResult of data){ + if (ruleResult.ruleName == o.code){ + ruleResult.success = false; + if (newObj.severity == "error"){ + ruleResult.class = "table-danger"; + } + else if (newObj.severity == "warn"){ + ruleResult.class = "table-warning"; + } + else { + ruleResult.class = "table-info"; + } + ruleResult.message = (ruleResult.details.length + 1) + " " + newObj.severity; + if (ruleResult.details.length > 0){ + ruleResult.message = ruleResult.message + "s"; + } + ruleResult.hasDetails = true; + ruleResult.details.push(newObj); + break; + } + } + }); + let jsonResult = { + "data": data, + "timestamp": new Date().toLocaleString(), + }; + let severityDist = Underscore.countBy(newResults, "severity"); + jsonResult.messageCount = results.length; + jsonResult.errorCount = severityDist.error || 0; + jsonResult.warnCount = severityDist.warn || 0; + jsonResult.infoCount = severityDist.info || 0; + jsonResult.hintCount = severityDist.hint || 0; + jsonResult.totalCount = totalRuleCount || 0; + jsonResult.enabledCount = enabledCount || 0; + return jsonResult; +} + +const getSeverityMap = () => { + const severityMap = {}; + severityMap[DiagnosticSeverity.Error] = "error"; + severityMap[DiagnosticSeverity.Warning] = "warn"; + severityMap[DiagnosticSeverity.Information] = "info"; + severityMap[DiagnosticSeverity.Hint] = "hint"; + return severityMap; +} + +const mapSeverity = (severity) => { + var severityValue = DiagnosticSeverity.Error; + switch(severity) + { + case "error": + severityValue = DiagnosticSeverity.Error; + break; + case "warn": + severityValue = DiagnosticSeverity.Warning; + break; + case "info": + severityValue = DiagnosticSeverity.Information; + break; + case "hint": + severityValue = DiagnosticSeverity.Hint; + break; + default: + break; + } + return severityValue; +} + +const generateReport = (jsonObj) => { + let htmlStr = loadFile(join(__dirname, "/templates/template.html")); + Handlebars.registerHelper("inc", (val) => { + return parseInt(val) + 1; + }); + let template = Handlebars.compile(htmlStr); + let data = template(jsonObj); + writeToFile(data, reportFileName); +} + +const linter = async (specFilePath, rulesetPath, followRefs) => { + const myDocument = new Document( + fs.readFileSync(specFilePath, "utf-8").trim(), + Parsers.Yaml, + specFilePath + ); + + const spectral = new Spectral( { resolver } ); + var tempFilePath = ""; + var isMultiRuleFiles = false; + + if (rulesetPath.length > 1) { + isMultiRuleFiles = true; + const masterRulesetContent = { + extends: rulesetPath.map(file => path.resolve(file)), + }; + const contentString = yaml.dump(masterRulesetContent, { indent: 2 }); + const tempDir = process.cwd(); + const tempFileName = `.spectral-${Date.now()}.yaml`; + tempFilePath = path.join(tempDir, tempFileName); + fs.writeFileSync(tempFilePath, contentString, 'utf8'); + } + else { + tempFilePath = path.resolve(rulesetPath[0]); + } + spectral.setRuleset(await bundleAndLoadRuleset(tempFilePath, { fs, fetch })); + var binded = {}; + + const runPromise = followRefs + ? spectral.run(myDocument, { resolve: true }) + : spectral.run(myDocument); + + await runPromise.then(results => { + binded.rules = spectral.ruleset.rules; + binded.results = results; + }); + + try { + if (isMultiRuleFiles && fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + } catch (cleanupErr) { + console.error(`Remove temporary file ${tempFilePath} failed. Please delete manually.`); + } + + return binded; +} + +const severeEnoughToFail = (results, failSeverity) => { + const diagnosticSeverity = mapSeverity(failSeverity); + return results.some(r => r.severity <= diagnosticSeverity); +} + +const consoleMsgPart = (results, consoleSeverity) => { + const diagnosticSeverity = mapSeverity(consoleSeverity); + return results.filter(r => r.severity <= diagnosticSeverity).sort((a, b) => a.severity - b.severity); +} + +const getSeverityMsgMap = () => { + const severityMap = {}; + severityMap[DiagnosticSeverity.Error] = "Error"; + severityMap[DiagnosticSeverity.Warning] = "Warning"; + severityMap[DiagnosticSeverity.Information] = "Information"; + severityMap[DiagnosticSeverity.Hint] = "Hint"; + return severityMap; +} + +const resolveRuleDependencyResult = (rules, results) => { + const dependsOnMap = {}; + for (const [ruleName, ruleDef] of Object.entries(rules)) { + const deps = ruleDef?.definition?.['x-dependsOn']; + if (deps) { + dependsOnMap[ruleName] = Array.isArray(deps) ? deps : [deps]; + } + } + + const allDepsCache = {}; + const getAllDependencies = (ruleName, visited = new Set()) => { + if (allDepsCache[ruleName]) return allDepsCache[ruleName]; + const directDeps = dependsOnMap[ruleName] || []; + const deps = new Set(); + + for (const dep of directDeps) { + if (visited.has(dep)) continue; + deps.add(dep); + const transitiveDeps = getAllDependencies(dep, new Set(visited).add(ruleName)); + for (const d of transitiveDeps) { + deps.add(d); + } + } + + allDepsCache[ruleName] = deps; + return deps; + }; + + const issuesByLocation = new Map(); + for (const issue of results) { + const locationKey = issue.path?.join('.') || ''; + if (!issuesByLocation.has(locationKey)) { + issuesByLocation.set(locationKey, []); + } + issuesByLocation.get(locationKey).push(issue); + } + + const filteredResults = []; + + for (const [_, issues] of issuesByLocation.entries()) { + const failedRules = new Set(issues.map(i => i.code)); + + for (const issue of issues) { + const ruleCode = issue.code; + const allDeps = getAllDependencies(ruleCode); + const hasFailedAncestor = [...allDeps].some(dep => failedRules.has(dep)); + + if (!hasFailedAncestor) { + filteredResults.push(issue); + } + } + } + + return filteredResults; +}; + +linter(specFilePath, rulesetPath, resolveRefs).then(linterResult => { + const spectralResult = resolveRuleDependencyResult(linterResult.rules, linterResult.results); + + const spectralReport = path.resolve(jsonFile); + fs.writeFileSync(spectralReport, JSON.stringify(spectralResult), { encoding: "utf8" }); + + const jsonResult = formatJSON(linterResult.rules, spectralResult); + generateReport(jsonResult); + const reportFilePath = path.resolve(reportFileName); + const isErrorSeverity = mapSeverity(failSeverity) === DiagnosticSeverity.Error; + const failed = severeEnoughToFail(spectralResult, failSeverity); + const consolePart = consoleMsgPart(spectralResult, consoleSeverity); + const consoleMsg = consolePart.map(error=> + `-------------------------------------------------- +${getSeverityMsgMap()[error.severity]}: +Line: [${error.range.start.line}:${error.range.start.character}-${error.range.end.line}:${error.range.end.character}] +Path: [${error.path.join('.')}] +Desc: [${error.code} ${error.message} ]`).join('\n'); + if (failed) + { + + process.stdout.write( + `Results with severity of '${failSeverity}' ${isErrorSeverity ? '' : 'or higher '}found!\n` + + consoleMsg + + `\n-------------------------------------------------- +Detailed report: ` + reportFilePath + '\n', + ); + process.exit(1); + } + else { + process.stdout.write( + `No results with a severity of '${failSeverity}' ${isErrorSeverity ? '' : 'or higher '}found!\n` + + consoleMsg + + `\n-------------------------------------------------- +Detailed report: ` + reportFilePath + '\n', + ); + process.exit(0); + } +}); + + diff --git a/api-linter/src/templates/template.html b/api-linter/src/templates/template.html new file mode 100644 index 0000000..36a141b --- /dev/null +++ b/api-linter/src/templates/template.html @@ -0,0 +1,161 @@ + + + + + Siemens Xcelerator-API Linter Report + + + + + + + +
+
+

Siemens API Linter Report

+
+
+
+
+ +
+
Total Rules Count: {{this.enabledCount}}
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SeverityProblems
Errors{{this.errorCount}}
Warnings{{this.warnCount}}
Info{{this.infoCount}}
Hints{{this.hintCount}}
+
+ +
+

Detailed report

+
+ +
+ + + + {{#each this.data}} + + + + + {{#if hasDetails}} + + {{else}} + + {{/if}} + + + + + + {{/each}} + +
+ {{#if hasDetails}} + + + + + + + + + + + + {{#each details}} + + + + + + {{/each}} + +
#LineSeverityPathmessage
{{inc @index}} + {{line}}{{severity}}{{path}}{{message}} +
+ {{/if}} +
+
+
+ + + + + + + \ No newline at end of file diff --git a/api-linter/test/api-commonops.test.js b/api-linter/test/api-commonops.test.js new file mode 100644 index 0000000..6f148eb --- /dev/null +++ b/api-linter/test/api-commonops.test.js @@ -0,0 +1,123 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyErrors, assertOnlyInfos } = require("./base.js") + +const path = 'commonops/'; +const rulesetPath = "test/testdata/commonops/.spectral.yml"; + +test('A Successful fetch of individual resources MUST return the status code 200', async () => { + const specFilePath = getSpecFilePath(path, "800-1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[800.1]"); + //Problem in line:29 + assertOnlyErrors(results); + }); +}) + +test('A successful fetch of resource collection request MUST return a 2xx status code', async () => { + const specFilePath = getSpecFilePath(path, "800-2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[800.2]"); + //Problem in line:37 + assertOnlyErrors(results); + }); +}) + +test('An API Provider MUST support fetching resource data for provided links', async () => { + const specFilePath = getSpecFilePath(path, "800-4.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[800.4]"); + //Problem in line:50 + assertOnlyErrors(results); + }); +}) + +test('The POST request MUST include a single resource object as primary data', async () => { + const specFilePath = getSpecFilePath(path, "801.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[801]"); + //Problem in line:45-47 + assertOnlyErrors(results); + }); +}) + +test('API MUST respond to successful POST creation request with 2xx success status code', async () => { + const specFilePath = getSpecFilePath(path, "801-4.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[801.4]"); + //Problem in line:34 + assertOnlyErrors(results); + }); +}) + +test('API MUST respond to successful PATCH update request with 2xx success status code', async () => { + const specFilePath = getSpecFilePath(path, "802-6.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[802.6]"); + //Problem in line:42 + assertOnlyErrors(results); + }); +}) + +test('API MUST respond to successful PUT update request with 2xx success status code', async () => { + const specFilePath = getSpecFilePath(path, "803-2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[803.2]"); + //Problem in line:42 + assertOnlyErrors(results); + }); +}) + +test('API MUST respond to successful DELETE request with 2xx success status code', async () => { + const specFilePath = getSpecFilePath(path, "804-1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[804.1]"); + //Problem in line:37 + assertOnlyErrors(results); + }); +}) + +test('The API consumer MAY use conditional header information to solve concurrent update requests', async () => { + const specFilePath = getSpecFilePath(path, "803-6.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[803.6]"); + //Problem in line:37-40 + assertOnlyInfos(results); + }); +}) + diff --git a/api-linter/test/api-errors-refs.test.js b/api-linter/test/api-errors-refs.test.js new file mode 100644 index 0000000..db05af4 --- /dev/null +++ b/api-linter/test/api-errors-refs.test.js @@ -0,0 +1,42 @@ +// test/api-errors-refs.test.js +const fs = require('fs'); +const { Document } = require('@stoplight/spectral-core'); +const Parsers = require('@stoplight/spectral-parsers'); + +// 👇 RE-ADD setupSpectral here +const { setupSpectral } = + require('@jamietanna/spectral-test-harness'); + +const { resultsForSeverity } = require('./base.js'); +const { resolver } = require('@stoplight/spectral-ref-resolver'); +const path = require('path'); + +const rulesetPath = 'test/testdata/refs/.spectral.yml'; +const specFilePath = path.join(__dirname, 'testdata/refs', '300.yml'); // ABSOLUTE + +test('API MUST use official HTTP status codes as intended', async () => { + // ───────────── arrange ───────────── + /* ── Spectral with resolver ── */ + const spectral = await setupSpectral(rulesetPath, { resolver }); + + /* ── Document with correct source (real path!) ── */ + const raw = fs.readFileSync(specFilePath, 'utf8'); + const document = new Document(raw, Parsers.Yaml, specFilePath); + // … but give it the real filename so $ref resolution works + document.source = specFilePath; + + // ───────────── act ───────────── + const results = await spectral.run( + document, + { resolve: { external: true } } + ); + + // ───────────── assert ───────────── + const errors = resultsForSeverity(results, 'Error'); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe('Siemens-API-[300]'); + //Problem in line:35-40 + const warnings = resultsForSeverity(results, 'Warning'); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe('Siemens-API-[301]'); +}); diff --git a/api-linter/test/api-errors.test.js b/api-linter/test/api-errors.test.js new file mode 100644 index 0000000..4cf0453 --- /dev/null +++ b/api-linter/test/api-errors.test.js @@ -0,0 +1,85 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyWarnings, assertOnlyInfos } = require("./base.js") + +const path = 'error/'; +const rulesetPath = "test/testdata/error/.spectral.yml"; + +test('API MUST use official HTTP status codes as intended', async () => { + const specFilePath = getSpecFilePath(path, "300.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[300]"); + //Problem in line:35-40 + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[301]"); + }); +}) + +test('SHOULD only use most common HTTP status codes', async () => { + const specFilePath = getSpecFilePath(path, "301.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + //Problem in line:35-40 + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[301]"); + assertOnlyWarnings(results); + }); +}) + + test('SHOULD use the most specific HTTP status code when returning errors', async () => { + const specFilePath = getSpecFilePath(path, "302.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + //Problem in line:130 + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[302]"); + assertOnlyWarnings(results); + }); + }) + + test('Error object SHOULD be represented according to the defined structure fields', async () => { + const specFilePath = getSpecFilePath(path, "305.1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + //Problem in line:110 + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[305]-1"); + assertOnlyWarnings(results); + }); + }) + + test('The error links resource object MAY contain the members: about, type', async () => { + const specFilePath = getSpecFilePath(path, "305.2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + //Problem in line:114-117 + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[305]-2"); + assertOnlyInfos(results) + }); + }) + + test('The source object SHOULD include one of the members or be omitted: pointer, parameter, header', async () => { + const specFilePath = getSpecFilePath(path, "305.3.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + //Problem in line:114-117 + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[305]-3"); + assertOnlyWarnings(results); + }); + }) \ No newline at end of file diff --git a/api-linter/test/api-filtering.test.js b/api-linter/test/api-filtering.test.js new file mode 100644 index 0000000..25f01ce --- /dev/null +++ b/api-linter/test/api-filtering.test.js @@ -0,0 +1,30 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyInfos, assertOnlyHints } = require("./base.js") + +const path = 'filtering/'; +const rulesetPath = "test/testdata/filtering/.spectral.yml"; + +test('An API provider MAY NOT support more than 4 parameters at once for selecting resources', async () => { + const specFilePath = getSpecFilePath(path, "400.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const hints = resultsForSeverity(results, "Hint"); + expect(hints).toHaveLength(2); + expect(hints[0].code).toBe("Siemens-API-[400]"); + expect(hints[1].code).toBe("Siemens-API-[401]"); + //Problem in line:29-62 + assertOnlyHints(results); + }); +}) + +test('The query parameter filter SHOULD be used to filter or query data', async () => { + const specFilePath = getSpecFilePath(path, "401.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(0); + assertOnlyInfos(results); + }); +}) \ No newline at end of file diff --git a/api-linter/test/api-linter-cli.test.js b/api-linter/test/api-linter-cli.test.js new file mode 100644 index 0000000..1fc1477 --- /dev/null +++ b/api-linter/test/api-linter-cli.test.js @@ -0,0 +1,60 @@ +const path = require('path'); +const fs = require('fs'); +const { spawnSync } = require('child_process'); + +/* ─────────── paths used by both tests ─────────── */ +const cli = path.join(__dirname, '..', 'src', 'index.js'); +const ruleset = path.join(__dirname, 'testdata', 'refs', '.spectral.yml'); +const spec = path.join(__dirname, 'testdata', 'refs', '300.yml'); + +/* handy wrapper to invoke the CLI */ +function runLinter({ resolveFlag, reportFile }) { + const args = [ + cli, + '--specPath', spec, + '--rulesetPath', ruleset, + '--failSeverity','error', + '--outputFilename', reportFile + ]; + if (resolveFlag) args.push('--resolve'); + + return spawnSync('node', args, { encoding: 'utf8' }); +} + +describe('api-linter CLI external $ref behaviour', () => { + const reportWith = path.join(__dirname, 'tmp-report-resolve.html'); + const reportWithout = path.join(__dirname, 'tmp-report-no-resolve.html'); + + /* clean-up generated HTML files after the suite finished */ + afterAll(() => { + [reportWith, reportWithout].forEach(f => { + if (fs.existsSync(f)) fs.unlinkSync(f); + }); + }); + + test('with --resolve follows external refs (no invalid-ref)', () => { + const { status, stdout } = runLinter({ + resolveFlag: true, + reportFile: reportWith + }); + + /* exit-code still 1 because the real rule fires */ + expect(status).toBe(1); + expect(stdout).toMatch(/Siemens-API-\[300]/); // rule present + expect(stdout).not.toMatch(/invalid-ref/); // resolver worked + expect(fs.existsSync(reportWith)).toBe(true); // report created + }); + + test('without --resolve produces invalid-ref error', () => { + const { status, stdout } = runLinter({ + resolveFlag: false, + reportFile: reportWithout + }); + + expect(status).toBe(1); + expect(stdout).toMatch(/Siemens-API-\[300]/); // rule present + expect(stdout).toMatch(/Siemens-API-\[301]/); // rule present + expect(stdout).not.toMatch(/invalid-ref/); // Spectral never tried to search for external references + expect(fs.existsSync(reportWithout)).toBe(true); + }); +}); \ No newline at end of file diff --git a/api-linter/test/api-mediatype.test.js b/api-linter/test/api-mediatype.test.js new file mode 100644 index 0000000..262c5df --- /dev/null +++ b/api-linter/test/api-mediatype.test.js @@ -0,0 +1,114 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyErrors, assertOnlyWarnings, assertOnlyInfos } = require("./base.js") + +const path = 'mediatype/'; +const rulesetPath = "test/testdata/mediatype/.spectral.yml"; + +test('Siemens Xcelerator recommends the media type application/json for exchanging data.', async () => { + const specFilePath = getSpecFilePath(path, "100.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[100]"); + //Problem in line:32 + assertOnlyInfos(results); + }); +}) + +test('The root of every document SHOULD contain a JSON object', async () => { + const specFilePath = getSpecFilePath(path, "101-1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[101.1]"); + //Problem in line:38 + assertOnlyWarnings(results); + }); +}) + +test('A response document SHOULD contain at least one of the following:data,error,meta,links.', async () => { + const specFilePath = getSpecFilePath(path, "101-2.1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[101.2]-1"); + //Problem in line:40-42, properties should contain at least one of data,error,meta,links + assertOnlyWarnings(results); + }); +}) + +test('A request document SHOULD contain at least data', async () => { + const specFilePath = getSpecFilePath(path, "101-2.2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[101.2]-2"); + //Problem in line:45-47, properties should contain at least data + assertOnlyWarnings(results); + }); +}) + +test('The data type MUST be compliant with the types defined', async () => { + const specFilePath = getSpecFilePath(path, "101-4-1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[101.4.1]"); + //Problem in line:87, should be one of boolean|object|array|integer|number|string + assertOnlyErrors(results); + }); +}) + +test('A meta object SHOULD be used to represent meta information as a JSON object', async () => { + const specFilePath = getSpecFilePath(path, "101-7-2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[101.7.2]"); + //Problem in line:77, should be type: object + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[101.7]"); + }); +}) + +test('Field names SHOULD use lower camel case', async () => { + const specFilePath = getSpecFilePath(path, "101-8.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[101.8]"); + //Problem in line:68, should be addressDetail + assertOnlyWarnings(results); + }); +}) + +test('Resource names SHOULD be in lowercase with hyphen', async () => { + const specFilePath = getSpecFilePath(path, "101-9.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[101.9]"); + //Problem in line:20, should be room-details + assertOnlyWarnings(results); + }); +}) + + + diff --git a/api-linter/test/api-pagination.test.js b/api-linter/test/api-pagination.test.js new file mode 100644 index 0000000..5d9dfa5 --- /dev/null +++ b/api-linter/test/api-pagination.test.js @@ -0,0 +1,102 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyInfos, assertOnlyHints } = require("./base.js") + +const path = 'pagination/'; +const rulesetPath = "test/testdata/pagination/.spectral.yml"; + +test('A server MAY provide links to traverse a paginated data set', async () => { + const specFilePath = getSpecFilePath(path, "600.1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[600]-1"); + assertOnlyInfos(results); + }); + }) + +test('Pagination links SHOULD appear in the top-level links object.', async () => { + const specFilePath = getSpecFilePath(path, "600.2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[600]-2"); + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[600]-1"); + }); +}) + +test('Naming convention MUST be used for pagination keys.', async () => { + const specFilePath = getSpecFilePath(path, "600.3.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const hints = resultsForSeverity(results, "Hint"); + expect(hints).toHaveLength(1); + expect(hints[0].code).toBe("Siemens-API-[600]-3"); + assertOnlyHints(results); + }); +}) + +test('Pagination SHOULD be implemented using query parameters.', async () => { + const specFilePath = getSpecFilePath(path, "601.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[601]"); + }); +}) + +test('Server MAY provide pagination meta information to the client [601.1.1].', async () => { + const specFilePath = getSpecFilePath(path, "601-1-1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[601.1.1]"); + assertOnlyInfos(results); + }); +}) + +test('Server MAY provide pagination meta information to the client [601.2.1].', async () => { + const specFilePath = getSpecFilePath(path, "601-2-1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[601.2.1]"); + assertOnlyInfos(results); + }); +}) + +test('Server MAY provide pagination meta information to the client [601.3.1].', async () => { + const specFilePath = getSpecFilePath(path, "601-3-1.1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[601.3.1]-1"); + assertOnlyInfos(results); + }); +}) + +test('Server MAY provide pagination meta information to the client [601.3.1].', async () => { + const specFilePath = getSpecFilePath(path, "601-3-1.2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[601.3.1]-2"); + assertOnlyInfos(results); + }); +}) \ No newline at end of file diff --git a/api-linter/test/api-security.test.js b/api-linter/test/api-security.test.js new file mode 100644 index 0000000..44a9ee5 --- /dev/null +++ b/api-linter/test/api-security.test.js @@ -0,0 +1,49 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyErrors } = require("./base.js") + +const path = 'security/'; +const rulesetPath = "test/testdata/security/.spectral.yml"; + +test('No security problem test 1', async () => { + global.apiSecurity = 'y'; + const specFilePath = getSpecFilePath(path, "valid.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + expect(results).toHaveLength(0); + }); +}) + +test('No security problem test 2', async () => { + global.apiSecurity = 'n'; + const specFilePath = getSpecFilePath(path, "invalid.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + expect(results).toHaveLength(0); + }); +}) + +test('No security problem test 3', async () => { + global.apiSecurity = 'n'; + const specFilePath = getSpecFilePath(path, "invalid.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + expect(results).toHaveLength(0); + }); +}) + +test('Has security problem test', async () => { + global.apiSecurity = 'y'; + const specFilePath = getSpecFilePath(path, "invalid.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + expect(results).toHaveLength(1); + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-Security"); + assertOnlyErrors(results); + }); +}) \ No newline at end of file diff --git a/api-linter/test/api-sparse-fieldsets.test.js b/api-linter/test/api-sparse-fieldsets.test.js new file mode 100644 index 0000000..fc5c0fa --- /dev/null +++ b/api-linter/test/api-sparse-fieldsets.test.js @@ -0,0 +1,17 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyInfos } = require("./base.js") + +const path = 'fields/'; +const rulesetPath = "test/testdata/fields/.spectral.yml"; + +test('API MAY provide projection of fieldset in response', async () => { + const specFilePath = getSpecFilePath(path, "500.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(1); + expect(infos[0].code).toBe("Siemens-API-[500]"); + assertOnlyInfos(results); + }); + }) diff --git a/api-linter/test/api-versioning.test.js b/api-linter/test/api-versioning.test.js new file mode 100644 index 0000000..002ecea --- /dev/null +++ b/api-linter/test/api-versioning.test.js @@ -0,0 +1,61 @@ +const { retrieveDocument, setupSpectral} = require('@jamietanna/spectral-test-harness') +const {resultsForSeverity, getSpecFilePath, assertOnlyErrors, assertOnlyWarnings } = require("./base.js") + +const path = 'versioning/'; +const rulesetPath = "test/testdata/versioning/.spectral.yml"; + +test('Semantic versioning MUST be used to version individual APIs', async () => { + global.apiVersioning = 'url'; + const specFilePath = getSpecFilePath(path, "semantic-versioning.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Semantic-Versioning-[2.0.0]"); + //Problem in line:35-40 + assertOnlyErrors(results); + }); +}) + +test('The Major version number MUST be specified in the URI as a path segment', async () => { + global.apiVersioning = 'url'; + const specFilePath = getSpecFilePath(path, "200-1.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[200.1]"); + //Problem in line:13 + assertOnlyErrors(results); + }); +}) + +test('A request HTTP header e.g., Api-Version MUST be supported to allow client to specify the version.', async () => { + global.apiVersioning = 'header'; + const specFilePath = getSpecFilePath(path, "200-2.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe("Siemens-API-[200.2]"); + //Problem in line:29 + assertOnlyErrors(results); + }); +}) + +test("A header with full semantic version value SHOULD be returned in the response with 'Api-Version: ..'", async () => { + global.apiVersioning = 'header'; + const specFilePath = getSpecFilePath(path, "201.yml"); + const spectral = await setupSpectral(rulesetPath); + const document = retrieveDocument(specFilePath); + spectral.run(document).then(results => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(1); + expect(warnings[0].code).toBe("Siemens-API-[201]"); + //Problem in line:44 + assertOnlyWarnings(results); + }); +}) \ No newline at end of file diff --git a/api-linter/test/base.js b/api-linter/test/base.js new file mode 100644 index 0000000..ab06f82 --- /dev/null +++ b/api-linter/test/base.js @@ -0,0 +1,54 @@ +const { DiagnosticSeverity } = require('@stoplight/types') + +function resultsForSeverity (results, severity) { + return results.filter((r) => DiagnosticSeverity[r.severity] === severity) +} + +const assertOnlyErrors = (results) => { + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(0); + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(0); + const hints = resultsForSeverity(results, "Hint"); + expect(hints).toHaveLength(0); +} + +const assertOnlyWarnings = (results) => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(0); + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(0); + const hints = resultsForSeverity(results, "Hint"); + expect(hints).toHaveLength(0); +} + +const assertOnlyInfos = (results) => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(0); + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(0); + const hints = resultsForSeverity(results, "Hint"); + expect(hints).toHaveLength(0); +} + +const assertOnlyHints = (results) => { + const errors = resultsForSeverity(results, "Error"); + expect(errors).toHaveLength(0); + const warnings = resultsForSeverity(results, "Warning"); + expect(warnings).toHaveLength(0); + const infos = resultsForSeverity(results, "Information"); + expect(infos).toHaveLength(0); +} + +const getSpecFilePath = (path, fileName) => { + return path + fileName; +} + +module.exports = { + resultsForSeverity, + getSpecFilePath, + assertOnlyErrors, + assertOnlyWarnings, + assertOnlyInfos, + assertOnlyHints +} diff --git a/api-linter/test/testdata/commonops/.spectral.yml b/api-linter/test/testdata/commonops/.spectral.yml new file mode 100644 index 0000000..3ea18f4 --- /dev/null +++ b/api-linter/test/testdata/commonops/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-common-operation.yml \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/800-1.yml b/api-linter/test/testdata/commonops/800-1.yml new file mode 100644 index 0000000..96c1ac5 --- /dev/null +++ b/api-linter/test/testdata/commonops/800-1.yml @@ -0,0 +1,77 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms/{id}": + get: + summary: Returns a Room + description: | + Query the specified Room according to the id parameter by this get action. + operationId: ReadRoom + tags: + - Rooms + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomReadResponse" +components: + schemas: + RoomReadResponse: + type: object + required: + - data + properties: + data: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/800-2.yml b/api-linter/test/testdata/commonops/800-2.yml new file mode 100644 index 0000000..900a4f4 --- /dev/null +++ b/api-linter/test/testdata/commonops/800-2.yml @@ -0,0 +1,94 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: Not-Api-Version + in: header + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "401": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/800-4.yml b/api-linter/test/testdata/commonops/800-4.yml new file mode 100644 index 0000000..3bb3443 --- /dev/null +++ b/api-linter/test/testdata/commonops/800-4.yml @@ -0,0 +1,77 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms/{id}": + get: + summary: Returns a Room + description: | + Query the specified Room according to the id parameter by this get action. + operationId: ReadRoom + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomReadResponse" +components: + schemas: + RoomReadResponse: + type: object + required: + - data + properties: + data: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + href: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/801-4.yml b/api-linter/test/testdata/commonops/801-4.yml new file mode 100644 index 0000000..93614f4 --- /dev/null +++ b/api-linter/test/testdata/commonops/801-4.yml @@ -0,0 +1,88 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms": + post: + summary: Creates a Room + description: | + Create a Room with the request content by this post action. + operationId: CreateRoom + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationRequest" + responses: + "301": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationResponse" +components: + schemas: + RoomCreationRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomBase" + RoomCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/801.yml b/api-linter/test/testdata/commonops/801.yml new file mode 100644 index 0000000..5c7f2c9 --- /dev/null +++ b/api-linter/test/testdata/commonops/801.yml @@ -0,0 +1,88 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms": + post: + summary: Creates a Room + description: | + Create a Room with the request content by this post action. + operationId: CreateRoom + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationResponse" +components: + schemas: + RoomCreationRequest: + type: array + required: + - content + properties: + content: + $ref: "#/components/schemas/RoomBase" + RoomCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/802-6.yml b/api-linter/test/testdata/commonops/802-6.yml new file mode 100644 index 0000000..250625a --- /dev/null +++ b/api-linter/test/testdata/commonops/802-6.yml @@ -0,0 +1,106 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Room resource. + schema: + type: string + example: 7eeea381-872c-4b83-a228-31878e5b8de8 + patch: + summary: Updates a Room + description: | + Update the specified Room according to the id parameter by this patch action. + operationId: UpdateRoom + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateRequest" + responses: + "301": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateResponse" +components: + schemas: + RoomUpdateRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/Room" + RoomUpdateResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + Room: + allOf: + - required: + - id + - type: object + properties: + id: + type: string + example: 846650de-20fd-4197-867b-00ac7606cffd + - $ref: "#/components/schemas/RoomBase" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/803-2.yml b/api-linter/test/testdata/commonops/803-2.yml new file mode 100644 index 0000000..087bd2a --- /dev/null +++ b/api-linter/test/testdata/commonops/803-2.yml @@ -0,0 +1,111 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Room resource. + schema: + type: string + example: 7eeea381-872c-4b83-a228-31878e5b8de8 + put: + summary: Updates a Room + description: | + Update the specified Room according to the id parameter by this put action. + operationId: UpdateRoom + parameters: + - name: If-Match + in: header + - name: If-None-Match + in: header + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateRequest" + responses: + "301": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateResponse" +components: + schemas: + RoomUpdateRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/Room" + RoomUpdateResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + Room: + allOf: + - required: + - id + - type: object + properties: + id: + type: string + example: 846650de-20fd-4197-867b-00ac7606cffd + - $ref: "#/components/schemas/RoomBase" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/803-6.yml b/api-linter/test/testdata/commonops/803-6.yml new file mode 100644 index 0000000..e5608be --- /dev/null +++ b/api-linter/test/testdata/commonops/803-6.yml @@ -0,0 +1,111 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Room resource. + schema: + type: string + example: 7eeea381-872c-4b83-a228-31878e5b8de8 + put: + summary: Updates a Room + description: | + Update the specified Room according to the id parameter by this put action. + operationId: UpdateRoom + tags: + - Rooms + parameters: + - name: NOT-If-Match + in: header + - name: NOT-If-None-Match + in: header + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateRequest" + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomUpdateResponse" +components: + schemas: + RoomUpdateRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/Room" + RoomUpdateResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + Room: + allOf: + - required: + - id + - type: object + properties: + id: + type: string + example: 846650de-20fd-4197-867b-00ac7606cffd + - $ref: "#/components/schemas/RoomBase" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + \ No newline at end of file diff --git a/api-linter/test/testdata/commonops/804-1.yml b/api-linter/test/testdata/commonops/804-1.yml new file mode 100644 index 0000000..f6ba6a9 --- /dev/null +++ b/api-linter/test/testdata/commonops/804-1.yml @@ -0,0 +1,38 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + "/rooms/{id}": + parameters: + - name: id + in: path + required: true + description: Identifier of a Room resource. + schema: + type: string + example: 7eeea381-872c-4b83-a228-31878e5b8de8 + delete: + summary: Deletes a Room + description: | + Delete the specified Room according to the id parameter by this delete action. + operationId: DeleteRoom + tags: + - Rooms + responses: + "404": + description: No content \ No newline at end of file diff --git a/api-linter/test/testdata/error/.spectral.yml b/api-linter/test/testdata/error/.spectral.yml new file mode 100644 index 0000000..e0af132 --- /dev/null +++ b/api-linter/test/testdata/error/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-error-reporting.yml \ No newline at end of file diff --git a/api-linter/test/testdata/error/300.yml b/api-linter/test/testdata/error/300.yml new file mode 100644 index 0000000..bb3751d --- /dev/null +++ b/api-linter/test/testdata/error/300.yml @@ -0,0 +1,117 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + "99": + description: errors + content: + application/json: + schema: + $ref: "#/components/schemas/Errors" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "99" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + detail: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. diff --git a/api-linter/test/testdata/error/301.yml b/api-linter/test/testdata/error/301.yml new file mode 100644 index 0000000..5cb0a6a --- /dev/null +++ b/api-linter/test/testdata/error/301.yml @@ -0,0 +1,117 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + "423": + description: errors + content: + application/json: + schema: + $ref: "#/components/schemas/Errors" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "423" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + detail: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. diff --git a/api-linter/test/testdata/error/302.yml b/api-linter/test/testdata/error/302.yml new file mode 100644 index 0000000..e1d453b --- /dev/null +++ b/api-linter/test/testdata/error/302.yml @@ -0,0 +1,133 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "423" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + detail: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. + responses: + DefaultErrors: + description: Standard errors + content: + application/json: + schema: + $ref: "#/components/schemas/Errors" + examples: + Forbidden: + $ref: "#/components/examples/Forbidden" + examples: + Forbidden: + summary: forbidden + value: + errors: + - id: 5fa91094-7caf-4ce7-85f5-25f7d883a7d4 + status: "421" + code: forbidden + title: Forbidden + detail: The provided authorization means did not contain suitable permissions. diff --git a/api-linter/test/testdata/error/305.1.yml b/api-linter/test/testdata/error/305.1.yml new file mode 100644 index 0000000..6486b27 --- /dev/null +++ b/api-linter/test/testdata/error/305.1.yml @@ -0,0 +1,133 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "423" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + info: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. + responses: + DefaultErrors: + description: Standard errors + content: + application/json: + schema: + $ref: "#/components/schemas/Errors" + examples: + Forbidden: + $ref: "#/components/examples/Forbidden" + examples: + Forbidden: + summary: forbidden + value: + errors: + - id: 5fa91094-7caf-4ce7-85f5-25f7d883a7d4 + status: "403" + code: forbidden + title: Forbidden + detail: The provided authorization means did not contain suitable permissions. diff --git a/api-linter/test/testdata/error/305.2.yml b/api-linter/test/testdata/error/305.2.yml new file mode 100644 index 0000000..83cdc6d --- /dev/null +++ b/api-linter/test/testdata/error/305.2.yml @@ -0,0 +1,139 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "423" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + detail: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + responses: + DefaultErrors: + description: Standard errors + content: + application/json: + schema: + $ref: "#/components/schemas/Errors" + examples: + Forbidden: + $ref: "#/components/examples/Forbidden" + examples: + Forbidden: + summary: forbidden + value: + errors: + - id: 5fa91094-7caf-4ce7-85f5-25f7d883a7d4 + status: "403" + code: forbidden + title: Forbidden + detail: The provided authorization means did not contain suitable permissions. diff --git a/api-linter/test/testdata/error/305.3.yml b/api-linter/test/testdata/error/305.3.yml new file mode 100644 index 0000000..f63fd75 --- /dev/null +++ b/api-linter/test/testdata/error/305.3.yml @@ -0,0 +1,139 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + default: + $ref: "#/components/responses/DefaultErrors" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "404" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + detail: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. + source: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses/822a6549-4fd8-478a-b5ed-73bef7a066f4" + responses: + DefaultErrors: + description: Standard errors + content: + application/json: + schema: + $ref: "#/components/schemas/Errors" + examples: + Forbidden: + $ref: "#/components/examples/Forbidden" + examples: + Forbidden: + summary: forbidden + value: + errors: + - id: 5fa91094-7caf-4ce7-85f5-25f7d883a7d4 + status: "403" + code: forbidden + title: Forbidden + detail: The provided authorization means did not contain suitable permissions. diff --git a/api-linter/test/testdata/fields/.spectral.yml b/api-linter/test/testdata/fields/.spectral.yml new file mode 100644 index 0000000..ae2fb30 --- /dev/null +++ b/api-linter/test/testdata/fields/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-sparse-fieldsets.yml \ No newline at end of file diff --git a/api-linter/test/testdata/fields/500.yml b/api-linter/test/testdata/fields/500.yml new file mode 100644 index 0000000..130dcbf --- /dev/null +++ b/api-linter/test/testdata/fields/500.yml @@ -0,0 +1,108 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "401": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/filtering/.spectral.yml b/api-linter/test/testdata/filtering/.spectral.yml new file mode 100644 index 0000000..4ece15f --- /dev/null +++ b/api-linter/test/testdata/filtering/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-filtering.yml \ No newline at end of file diff --git a/api-linter/test/testdata/filtering/400.yml b/api-linter/test/testdata/filtering/400.yml new file mode 100644 index 0000000..6691952 --- /dev/null +++ b/api-linter/test/testdata/filtering/400.yml @@ -0,0 +1,121 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: field + in: query + required: true + description: Identifier of a Course resource. + schema: + type: string + - name: param + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/filtering/401.yml b/api-linter/test/testdata/filtering/401.yml new file mode 100644 index 0000000..f384313 --- /dev/null +++ b/api-linter/test/testdata/filtering/401.yml @@ -0,0 +1,121 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: field + in: query + required: true + description: Identifier of a Course resource. + schema: + type: string + - name: filter + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/.spectral.yml b/api-linter/test/testdata/mediatype/.spectral.yml new file mode 100644 index 0000000..4513222 --- /dev/null +++ b/api-linter/test/testdata/mediatype/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-media-type.yml \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/100.yml b/api-linter/test/testdata/mediatype/100.yml new file mode 100644 index 0000000..7278a7f --- /dev/null +++ b/api-linter/test/testdata/mediatype/100.yml @@ -0,0 +1,80 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/vndjson: + schema: + $ref: "#/components/schemas/RoomsReadResponse" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/101-1.yml b/api-linter/test/testdata/mediatype/101-1.yml new file mode 100644 index 0000000..afc8584 --- /dev/null +++ b/api-linter/test/testdata/mediatype/101-1.yml @@ -0,0 +1,80 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" +components: + schemas: + RoomsReadResponse: + type: array + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/101-2.1.yml b/api-linter/test/testdata/mediatype/101-2.1.yml new file mode 100644 index 0000000..3861312 --- /dev/null +++ b/api-linter/test/testdata/mediatype/101-2.1.yml @@ -0,0 +1,81 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" +components: + schemas: + RoomsReadResponse: + type: object + required: + - items + properties: + items: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/101-2.2.yml b/api-linter/test/testdata/mediatype/101-2.2.yml new file mode 100644 index 0000000..3181532 --- /dev/null +++ b/api-linter/test/testdata/mediatype/101-2.2.yml @@ -0,0 +1,88 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + post: + summary: Creates a Room + description: | + Create a Room with the request content by this post action. + operationId: CreateRoom + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationResponse" +components: + schemas: + RoomCreationRequest: + type: object + required: + - item + properties: + item: + $ref: "#/components/schemas/RoomBase" + RoomCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/101-4-1.yml b/api-linter/test/testdata/mediatype/101-4-1.yml new file mode 100644 index 0000000..12f2caf --- /dev/null +++ b/api-linter/test/testdata/mediatype/101-4-1.yml @@ -0,0 +1,88 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + post: + summary: Creates a Room + description: | + Create a Room with the request content by this post action. + operationId: CreateRoom + tags: + - Rooms + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/RoomCreationResponse" +components: + schemas: + RoomCreationRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomBase" + RoomCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: double + example: 85734 + \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/101-7-2.yml b/api-linter/test/testdata/mediatype/101-7-2.yml new file mode 100644 index 0000000..d3b4387 --- /dev/null +++ b/api-linter/test/testdata/mediatype/101-7-2.yml @@ -0,0 +1,168 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + parameters: + - name: cursor + in: query + description: Opaque cursor to fetch specific page + schema: + type: string + default: first page + - name: limit + in: query + description: Desired maximum number of elements to be returned. A server may return less elements. + schema: + type: integer + default: 1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + - meta + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + $ref: "#/components/schemas/RoomPageCursor" + meta: + type: array + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + RoomCreationRequest: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomBase" + RoomCreationResponse: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/RoomLink" + RoomLink: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85734" + RoomPageCursor: + type: object + description: Pagination links for cursor-based pagination. + properties: + self: + type: string + example: https://api.siemens.com/reference/api/rooms?limit=1 + description: | + Opaque cursor to fetch next elements. To be used as value of query parameter cursor. + Is only added if more elements are present. + next: + type: string + example: https://api.siemens.com/reference/api/rooms?cursor=cXdlcnR5&limit=1 + description: Returned the next elements after the cursor. + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - number + - size + - totalPages + - totalElements + properties: + number: + type: integer + example: 1 + description: Current page number, starting from 1. + size: + type: integer + example: 1 + description: Returned number of elements. + totalPages: + type: integer + example: 100 + description: Total amount of pages available. + totalElements: + type: integer + example: 100 + description: Total amount of elements available. + \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/101-8.yml b/api-linter/test/testdata/mediatype/101-8.yml new file mode 100644 index 0000000..28eb6ff --- /dev/null +++ b/api-linter/test/testdata/mediatype/101-8.yml @@ -0,0 +1,81 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" +components: + schemas: + RoomsReadResponse: + type: object + required: + - items + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address-detail: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + \ No newline at end of file diff --git a/api-linter/test/testdata/mediatype/101-9.yml b/api-linter/test/testdata/mediatype/101-9.yml new file mode 100644 index 0000000..5d1c626 --- /dev/null +++ b/api-linter/test/testdata/mediatype/101-9.yml @@ -0,0 +1,81 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /roomDetails: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" +components: + schemas: + RoomsReadResponse: + type: object + required: + - items + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/.spectral.yml b/api-linter/test/testdata/pagination/.spectral.yml new file mode 100644 index 0000000..a2ac465 --- /dev/null +++ b/api-linter/test/testdata/pagination/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-pagination.yml \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/600.1.yml b/api-linter/test/testdata/pagination/600.1.yml new file mode 100644 index 0000000..c91d4c6 --- /dev/null +++ b/api-linter/test/testdata/pagination/600.1.yml @@ -0,0 +1,161 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + $ref: "#/components/schemas/RoomPageCursor" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomPageCursor: + type: object + description: Pagination links for cursor-based pagination. + properties: + notself: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + notfirst: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + notlast: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + notnext: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - number + - size + - totalPages + properties: + number: + type: integer + example: 1 + description: Current page number, starting from 1. + size: + type: integer + example: 1 + description: Returned number of elements. + totalPages: + type: integer + example: 100 + description: Total amount of pages available. + elements: + type: integer + example: 100 + description: Amount of elements available. + totalElements: + type: integer + example: 100 + description: Total amount of elements available. \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/600.2.yml b/api-linter/test/testdata/pagination/600.2.yml new file mode 100644 index 0000000..bfc6849 --- /dev/null +++ b/api-linter/test/testdata/pagination/600.2.yml @@ -0,0 +1,167 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + $ref: "#/components/schemas/RoomPageCursor" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomPageCursor: + type: object + description: Pagination links for cursor-based pagination. + properties: + page: + $ref: "#/components/schemas/RoomPageCursor2" + RoomPageCursor2: + type: object + description: Pagination links for cursor-based pagination. + properties: + self: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + first: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + last: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + next: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - number + - size + - totalPages + properties: + number: + type: integer + example: 1 + description: Current page number, starting from 1. + size: + type: integer + example: 1 + description: Returned number of elements. + totalPages: + type: integer + example: 100 + description: Total amount of pages available. + elements: + type: integer + example: 100 + description: Amount of elements available. + totalElements: + type: integer + example: 100 + description: Total amount of elements available. \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/600.3.yml b/api-linter/test/testdata/pagination/600.3.yml new file mode 100644 index 0000000..ac2c831 --- /dev/null +++ b/api-linter/test/testdata/pagination/600.3.yml @@ -0,0 +1,161 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + $ref: "#/components/schemas/RoomPageCursor" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" + RoomPageCursor: + type: object + description: Pagination links for cursor-based pagination. + properties: + self: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + first: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + notlast: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + next: + type: string + example: https://api.siemens.com/reference/api/rooms?number=1&size=1 + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - number + - size + - totalPages + properties: + number: + type: integer + example: 1 + description: Current page number, starting from 1. + size: + type: integer + example: 1 + description: Returned number of elements. + totalPages: + type: integer + example: 100 + description: Total amount of pages available. + elements: + type: integer + example: 100 + description: Amount of elements available. + totalElements: + type: integer + example: 100 + description: Total amount of elements available. \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/601-1-1.yml b/api-linter/test/testdata/pagination/601-1-1.yml new file mode 100644 index 0000000..4f0f22e --- /dev/null +++ b/api-linter/test/testdata/pagination/601-1-1.yml @@ -0,0 +1,136 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: cursor + in: query + description: Opaque cursor to fetch specific page + schema: + type: string + default: first page + - name: limit + in: query + description: Desired maximum number of elements to be returned. A server may return less elements. + schema: + type: integer + default: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - next + properties: + next: + type: string + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/601-2-1.yml b/api-linter/test/testdata/pagination/601-2-1.yml new file mode 100644 index 0000000..456b1dc --- /dev/null +++ b/api-linter/test/testdata/pagination/601-2-1.yml @@ -0,0 +1,148 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: offset + in: query + description: Opaque cursor to fetch specific page + schema: + type: integer + default: 0 + - name: limit + in: query + description: Desired maximum number of elements to be returned. A server may return less elements. + schema: + type: integer + default: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - elements + - offset + - totalElements + properties: + # elements: + # type: integer + # example: 1 + # description: number of records in the current page. + offset: + type: integer + example: 0 + description: current offset. + totalElements: + type: integer + example: 100 + description: Total amount of elements available. + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/601-3-1.1.yml b/api-linter/test/testdata/pagination/601-3-1.1.yml new file mode 100644 index 0000000..5fc9914 --- /dev/null +++ b/api-linter/test/testdata/pagination/601-3-1.1.yml @@ -0,0 +1,148 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + description: Opaque cursor to fetch specific page + schema: + type: integer + default: 1 + - name: size + in: query + description: Desired maximum number of elements to be returned. A server may return less elements. + schema: + type: integer + default: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - number + - size + - totalPages + properties: + # number: + # type: integer + # example: 1 + # description: Current page number, starting from 1. + size: + type: integer + example: 1 + description: Returned number of elements. + totalPages: + type: integer + example: 100 + description: Total amount of pages available. + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/601-3-1.2.yml b/api-linter/test/testdata/pagination/601-3-1.2.yml new file mode 100644 index 0000000..17be859 --- /dev/null +++ b/api-linter/test/testdata/pagination/601-3-1.2.yml @@ -0,0 +1,156 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + description: Opaque cursor to fetch specific page + schema: + type: integer + default: 1 + - name: size + in: query + description: Desired maximum number of elements to be returned. A server may return less elements. + schema: + type: integer + default: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + meta: + type: object + required: + - page + properties: + page: + $ref: "#/components/schemas/PageIndex" + PageIndex: + type: object + description: Pagination metadata for index-based pagination. + required: + - number + - size + - totalPages + properties: + number: + type: integer + example: 1 + description: Current page number, starting from 1. + size: + type: integer + example: 1 + description: Returned number of elements. + totalPages: + type: integer + example: 100 + description: Total amount of pages available. + elements: + type: integer + example: 1 + description: Representing the number of records returned in the current page. + # totalElements: + # type: integer + # example: 1 + # description: Representing the total number of elements. + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/pagination/601.yml b/api-linter/test/testdata/pagination/601.yml new file mode 100644 index 0000000..87946ba --- /dev/null +++ b/api-linter/test/testdata/pagination/601.yml @@ -0,0 +1,109 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + first: + type: string + example: "https://api.siemens.com/reference/api/courses?number=1&size=1" + last: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + next: + type: string + example: "https://api.siemens.com/reference/api/courses?number=2&size=1" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/refs/.spectral.yml b/api-linter/test/testdata/refs/.spectral.yml new file mode 100644 index 0000000..e0af132 --- /dev/null +++ b/api-linter/test/testdata/refs/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-error-reporting.yml \ No newline at end of file diff --git a/api-linter/test/testdata/refs/300.yml b/api-linter/test/testdata/refs/300.yml new file mode 100644 index 0000000..155c0cb --- /dev/null +++ b/api-linter/test/testdata/refs/300.yml @@ -0,0 +1,86 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + "99": + description: errors + content: + application/json: + schema: + $ref: "./common_errors.yml#/components/schemas/Errors" +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" diff --git a/api-linter/test/testdata/refs/common_errors.yml b/api-linter/test/testdata/refs/common_errors.yml new file mode 100644 index 0000000..eeb6cfe --- /dev/null +++ b/api-linter/test/testdata/refs/common_errors.yml @@ -0,0 +1,48 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api +paths: + +components: + schemas: + Errors: + type: object + required: + - errors + properties: + errors: + type: array + items: + title: Error + type: object + properties: + id: + type: string + description: Unique identifier for this particular occurrence of the error. + example: df873142-804e-4146-8956-02682c20d23d + status: + type: string + description: HTTP status code applicable to this error. + example: "99" + code: + type: string + description: Unique identifier for this type of error. + example: exampleError + title: + type: string + description: Short, human-readable summary of the problem associated to exactly one error code. May be localized. + example: Example error + detail: + type: string + description: Human-readable explanation specific to this occurrence of the error. May be localized. + example: This is an example error which occured for resource of type example. diff --git a/api-linter/test/testdata/security/.spectral.yml b/api-linter/test/testdata/security/.spectral.yml new file mode 100644 index 0000000..a6a6f68 --- /dev/null +++ b/api-linter/test/testdata/security/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-security.yml \ No newline at end of file diff --git a/api-linter/test/testdata/security/invalid.yml b/api-linter/test/testdata/security/invalid.yml new file mode 100644 index 0000000..4d28497 --- /dev/null +++ b/api-linter/test/testdata/security/invalid.yml @@ -0,0 +1,122 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + # parameters: + # - name: Authorization + # in: header + # required: true + # description: The security token signature. + # schema: + # type: string + # example: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2VuLWlzc3Vlci5zaWVtZW5zLmNvbSIsImlhdCI6MTY5OTg3NTc1OCwiZXhwIjoxNzMxNDExNzU4LCJhdWQiOiJodHRwczovL2V3cy5zaWVtZW5zLmNvbSIsInN1YiI6ImpvaG5AZG9lLmNvbSIsInNjb3BlIjpbImV3cy53ZWF0aGVyLnJlYWQiLCJld3Mud2VhdGhlci5hZG1pbiJdfQ.WwTZepkPTHrCysZ9AiLuN0k7QSDRFs-z4CxN9cvYDgA + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: field + in: query + required: true + description: Identifier of a Course resource. + schema: + type: string + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/security/valid.yml b/api-linter/test/testdata/security/valid.yml new file mode 100644 index 0000000..fddaf12 --- /dev/null +++ b/api-linter/test/testdata/security/valid.yml @@ -0,0 +1,122 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + parameters: + - name: Authorization + in: header + required: true + description: The security token signature. + schema: + type: string + example: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2VuLWlzc3Vlci5zaWVtZW5zLmNvbSIsImlhdCI6MTY5OTg3NTc1OCwiZXhwIjoxNzMxNDExNzU4LCJhdWQiOiJodHRwczovL2V3cy5zaWVtZW5zLmNvbSIsInN1YiI6ImpvaG5AZG9lLmNvbSIsInNjb3BlIjpbImV3cy53ZWF0aGVyLnJlYWQiLCJld3Mud2VhdGhlci5hZG1pbiJdfQ.WwTZepkPTHrCysZ9AiLuN0k7QSDRFs-z4CxN9cvYDgA + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: number + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: size + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: sort + in: query + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + - name: field + in: query + required: true + description: Identifier of a Course resource. + schema: + type: string + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/versioning/.spectral.yml b/api-linter/test/testdata/versioning/.spectral.yml new file mode 100644 index 0000000..0ab46a9 --- /dev/null +++ b/api-linter/test/testdata/versioning/.spectral.yml @@ -0,0 +1,2 @@ +extends: + - ../../../rulesets/xcelerator-api-versioning.yml \ No newline at end of file diff --git a/api-linter/test/testdata/versioning/200-1.yml b/api-linter/test/testdata/versioning/200-1.yml new file mode 100644 index 0000000..3895be3 --- /dev/null +++ b/api-linter/test/testdata/versioning/200-1.yml @@ -0,0 +1,18 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1.1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. \ No newline at end of file diff --git a/api-linter/test/testdata/versioning/200-2.yml b/api-linter/test/testdata/versioning/200-2.yml new file mode 100644 index 0000000..19ac06f --- /dev/null +++ b/api-linter/test/testdata/versioning/200-2.yml @@ -0,0 +1,94 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: Not-Api-Version + in: header + required: true + description: Identifier of a Course resource. + schema: + type: integer + example: 1 + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/versioning/201.yml b/api-linter/test/testdata/versioning/201.yml new file mode 100644 index 0000000..0161346 --- /dev/null +++ b/api-linter/test/testdata/versioning/201.yml @@ -0,0 +1,94 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. +paths: + /rooms: + get: + summary: Queries Rooms + description: | + Query a collection of Rooms with the request by this get action. + operationId: ReadRooms + tags: + - Rooms + parameters: + - name: Api-Version + in: header + required: true + description: Identifier of a Course resource. + schema: + type: string + example: 275bd2a1-e6cb-4ec3-80f3-22167c6bcaab + responses: + "200": + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/RoomsReadResponse" + headers: + "Not-Api-Version": + description: The API semantic-version string + schema: + type: string + example: 1.2.1 +components: + schemas: + RoomsReadResponse: + type: object + required: + - data + properties: + data: + type: array + items: + allOf: + - $ref: "#/components/schemas/RoomBase" + - type: object + properties: + links: + type: object + properties: + self: + type: string + example: "https://api.siemens.com/reference/api/rooms/846650de-20fd-4197-867b-00ac7606cffd" + description: "#servers/url" + lessons: + type: string + example: "https://api.siemens.com/reference/api/lessons/d8272c80-62b1-43b3-ba84-02cbe0aab5a2" + description: "#servers/url" + RoomBase: + type: object + description: Place where course lessons take place. + properties: + code: + type: string + description: Code of the room in a building or site. + example: MI 07.02.013 + address: + type: object + description: Address of the room. + properties: + street: + type: string + example: Boltzmannstr. 3 + city: + type: string + example: Garching b. München + zip: + type: string + example: "85748" \ No newline at end of file diff --git a/api-linter/test/testdata/versioning/semantic-versioning.yml b/api-linter/test/testdata/versioning/semantic-versioning.yml new file mode 100644 index 0000000..7e09ee3 --- /dev/null +++ b/api-linter/test/testdata/versioning/semantic-versioning.yml @@ -0,0 +1,18 @@ +openapi: 3.0.1 +info: + title: Reference OpenAPI Specification + version: 2.0 + contact: + name: API Guidance Team + url: https://api.siemens.com + email: noreply@siemens.com + description: | + This is an example API specification serving as reference for designing and + specifying APIs according to the API guidelines. +servers: + - url: https://api.siemens.com/reference/api/v1 +tags: + - name: Rooms + description: | + Rooms describe the locations where lessons take place. A room can only be + deleted when there are no related lessons. \ No newline at end of file diff --git a/package.json b/package.json index e00262c..4ffdae8 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "eslint-config-typescript", "eslint-plugin-defaultvalue", "prettier-config", - "stylelint-config-scss" + "stylelint-config-scss", + "api-linter" ] }