diff --git a/.spectral.yml b/.spectral.yml index cf5b7e7..81ad34b 100644 --- a/.spectral.yml +++ b/.spectral.yml @@ -6,6 +6,7 @@ extends: [spectral:oas] functions: + - date - requireExampleOrRef - requireRequestBodyDescription @@ -282,6 +283,89 @@ rules: functionOptions: match: ^[a-z0-9]+(-[a-z0-9]+)*$ + # ------------------------------------------------------------------------- + # 2.5 Lifecycle + # ------------------------------------------------------------------------- + + ## On API level + entur-stability-level-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.info.x-stability-level + then: + function: enumeration + functionOptions: + values: [draft, beta, stable] + + entur-deprecation-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.info.x-deprecated + then: + function: schema + functionOptions: + schema: + type: boolean + + entur-sunset-format-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $[?(@property === "info" && @["x-deprecated"] === true)] + then: + field: x-sunset + function: date + + entur-sunset-defined-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error # Or should this just be a warning? + given: $[?(@property === "info" && @["x-deprecated"] === true)] + then: + - field: x-sunset + function: schema + functionOptions: + schema: + type: string + + entur-sunset-undefined-api: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $[?(@property === "info" && @["x-deprecated"] !== true)] + then: + field: x-sunset + function: undefined + + # On individual operation level + entur-stability-level-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.paths.*[get,post,put,patch,delete,options,head,trace].x-stability-level + then: + function: enumeration + functionOptions: + values: [draft, beta, stable] + + entur-sunset-format-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.paths.*[get,post,put,patch,delete,options,head,trace].x-sunset + then: + function: date + + entur-sunset-defined-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#254-lifecycle" + severity: error # Or should this just be a warning? + given: $.paths.[?(@.deprecated === true)].x-sunset + then: + function: defined + + entur-sunset-undefined-operation: + documentationUrl: "https://github.com/entur/api-guidelines/blob/main/guidelines.md#25-lifecycle" + severity: error + given: $.paths.[?(@.deprecated !== true)].x-sunset + message: "\"x-sunset\" should only be set when deprecated is true" + then: + function: "undefined" + # ============================================================================= # 3. Naming & Structure Conventions @@ -363,12 +447,6 @@ rules: # 3.2 Versioning # ------------------------------------------------------------------------- - - # ------------------------------------------------------------------------- - # 3.3 Backward Compatibility - Not directly lintable - # ------------------------------------------------------------------------- - - # ============================================================================= # 4. Communication Standards # ============================================================================= diff --git a/functions/date.js b/functions/date.js new file mode 100644 index 0000000..8a00623 --- /dev/null +++ b/functions/date.js @@ -0,0 +1,14 @@ +module.exports = (targetVal, options, context) => { + if (typeof targetVal !== "string") { + return [{ message: "Must be a valid calendar date."}] + } + if (targetVal.match(/^\d{4}-\d{2}-\d{2}$/) == null) { + return [{ message: "Must be a valid calendar date."}] + } + + if (isNaN(Date.parse(targetVal))) { + return [{ message: "Must be a valid calendar date."}] + } + + return [] +} \ No newline at end of file diff --git a/guidelines.md b/guidelines.md index 126aa5f..1e0a542 100644 --- a/guidelines.md +++ b/guidelines.md @@ -262,6 +262,67 @@ Here, only 1 specification will be shown on the Developer Portal, which is a com +### 2.5 Lifecycle +All APIs, and endpoints within an API, follow a lifecycle with different stages that have different properties. The following sections describe a standardized way to declare an API's lifecycle status in its specification. Following this standard has several benefits: +- It is easier for consumers to understand what they can expect from an API in terms of stability. +- It is easier to enforce Enturs common policy on backwards compatibility and deprecation. + +Lifecycle status may be declared both for the entire API specification, or for individual operations (endpoints). At the API level, use the following extension fields in the `info` block: + +Field name |Type |Description +------------------|---------|----------- +x-stability-level |`string` | The current lifecycle status of the API. Must be one of: `draft`, `beta`, `stable`. Default value is `stable`. +x-deprecated |`boolean`| `true` if the API is deprecated. Default value is `false`. +x-sunset |`string` | The date at which the API will be discontinued, in the format `YYYY-MM-DD`. + +For individual operations, use the following extension fields in an [Operation object](https://spec.openapis.org/oas/v3.1.2.html#operation-object). All of the fields inherit default values from the API level. + +Field name |Type |Description +------------------|---------|----------- +deprecated |`boolean`| Already exists in the OAS. `true` if the endpoint is deprecated. Default value is `info.x-deprecated`. +x-stability-level |`string` | The current lifecycle status of the endpoint. Must be one of: `draft`, `beta`, `stable`. Default value is `info.x-stability-level`. +x-sunset |`string` | The date at which the endpoint will be discontinued, in the format `YYYY-MM-DD`. Default value is `info.x-sunset`. + +Below is a description of what values these fields should have during an API's or endpoint's lifecycle. + +#### 2.5.1 Design and development +The shape of the API is being figured out. Changes happen often. The API may not even be implemented yet. +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `draft` +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `false` (or be empty) +- :ballot_box_with_check: The field `x-sunset` **MUST** be empty +- :ballot_box_with_check: You **MAY** make [breaking changes](#I-breaking-changes) + +#### 2.5.2 Testing and feedback +Changes happen, and more design and development may be necessary. +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `beta` +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `false` (or be empty) +- :ballot_box_with_check: The field `x-sunset` **MUST** be empty +- :ballot_box_with_check: You **MAY** make [breaking changes](#I-breaking-changes) + +#### 2.5.3 Production use +The API is relied upon by consumers. Changes may happen, as long as they are not breaking. +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `stable` (or be empty) +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `false` (or be empty) +- :ballot_box_with_check: The field `x-sunset` **MUST** be empty +- :ballot_box_with_check: You **MUST NOT** make [breaking changes](#I-breaking-changes) + +#### 2.5.4 Deprecation +The API is marked as deprecated and consumers are encouraged to move to a new version. Only critical changes will happen. +- :ballot_box_with_check: The field `deprecated`/`x-deprecated` **MUST** have the value `true` +- :ballot_box_with_check: The field `x-sunset` **MUST** filled with a date that follows Enturs rules for deprecation. +- :ballot_box_with_check: The field `x-stability-level` **MUST** have the value `stable` (or be empty) +- :ballot_box_with_check: You **MUST NOT** make [breaking changes](#I-breaking-changes) + +#### 2.5.5 Sunsetting +The API is removed. +- :ballot_box_with_check: When the date in `x-sunset` has passed, the endpoint or API **SHOULD** be removed. If the API is not ready to be removed yet, the value should be updated. + +#### 2.5.6 Notes +- :ballot_box_with_check: Individual endpoints **MAY** override `x-stability-level` to an earlier stage in the lifecycle than the API, but **NOT** later. + - For example, if `stable` is declared at the API level, endpoints may be `draft`, `beta` or `stable`, but if `beta` is declared at the API level, endpoints may only be `draft` or `beta`. +- :ballot_box_with_check: Individual endpoints **MUST NOT** set `deprecated` to `false`, if the API has set `x-deprecated` to `true`. +- :ballot_box_with_check: Individual endpoints **MAY** override `x-sunset` to an earlier date than the API, but **NOT** later. + ## 3. Naming & Structure Conventions @@ -292,12 +353,7 @@ published, the current year and month is used, and MICRO is set to "00". If a ve If the published spec is equal to the current spec, no new version is created. Because versioning is done automatically, the value in `info.version` is ignored, but must be set in order for linting to pass, therefore a placeholder value like `1.0.0` may be used. -### 3.3 Backward Compatibility -- :eyes: You **MUST** not remove or modify existing fields or endpoints -- :eyes: You **MUST** introduce new versions for changes that break previous contracts -- :eyes: You **MUST** clearly document which features are deprecated and provide guidance for migration - -### 3.4 Tags Naming +### 3.3 Tags Naming :eyes: Tags should be used as a logical grouping that reflects functional domains, use cases or data types, not internal architecture. - Bad example: `internal`, `partner`, `production`, `route-service-v2`, `misc`, `other` - Good example, when using tags for functional domains: `Journey Planning`, `Realtime Departures` @@ -556,10 +612,14 @@ There may be situations where a pure REST architecture is not the best solution. - **WebSocket**: For real-time bidirectional communication - **gRPC**: For high-performance, strongly-typed services - ## FAQ *Must existing APIs conform the guidelines?* - Non-breaking changes (like adding example values) **SHOULD** be updated to be compliant with the guidelines. - Breaking changes **MAY** be added in a new version of the API. - New APIs **MUST** follow the guidelines. + +## Appendix + +## I. Breaking changes +Here we should write about what we mean with breaking changes.