Repository: stoplightio/spectral (ts)

Issue:
`oas3-valid-oas-content-example` rule needs to obey readOnly/writeOnly property
**Describe the bug**

According to https://swagger.io/docs/specification/data-models/data-types/ :
> If a readOnly or writeOnly property is included in the required list, required affects just the relevant scope – responses only or requests only. That is, read-only required properties apply to responses only, and write-only required properties – to requests only.

The rule `oas3-valid-oas-content-example` should not raise error when a `readOnly` and `required` property is missing in request example. And also `writeOnly` property isn't required in response example.

The rule `oas3-valid-media-example` has the same bug as well.

**To Reproduce**

1. Given this OpenAPI/AsyncAPI document [index.yaml](https://github.com/stoplightio/spectral/files/4889475/index.yaml.txt)
2. Run this CLI command `spectral lint index.yaml`
3. See error
> 16:21    error  oas3-valid-oas-content-example  Object should have required property `self`

**Expected behavior**
This error report is not expected.

The `self` property is readOnly, it isn't required in request.

**Environment (remove any that are not applicable):**
 - Library version: 5.4.0


Pull request description:
fix(rulesets): required readOnly and writeOnly properties should not …
…be considered required for respectively request and response bodies

Fixes #1274.

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No



Patch:
diff --git a/packages/rulesets/src/oas/functions/oasExample.ts b/packages/rulesets/src/oas/functions/oasExample.ts
index 9997b744..c51144e8 100644
--- a/packages/rulesets/src/oas/functions/oasExample.ts
+++ b/packages/rulesets/src/oas/functions/oasExample.ts
@@ -11,6 +11,10 @@ export type Options = {
   type: 'media' | 'schema';
 };
 
+type HasRequiredProperties = traverse.SchemaObject & {
+  required?: string[];
+};
+
 type MediaValidationItem = {
   field: string;
   multiple: boolean;
@@ -39,6 +43,22 @@ const MEDIA_VALIDATION_ITEMS: Dictionary<MediaValidationItem[], 2 | 3> = {
   ],
 };
 
+const REQUEST_MEDIA_PATHS: Dictionary<JsonPath[], 2 | 3> = {
+  2: [],
+  3: [
+    ['components', 'requestBodies'],
+    ['paths', '*', '*', 'requestBody'],
+  ],
+};
+
+const RESPONSE_MEDIA_PATHS: Dictionary<JsonPath[], 2 | 3> = {
+  2: [['responses'], ['paths', '*', '*', 'responses']],
+  3: [
+    ['components', 'responses'],
+    ['paths', '*', '*', 'responses'],
+  ],
+};
+
 const SCHEMA_VALIDATION_ITEMS: Dictionary<string[], 2 | 3> = {
   2: ['example', 'x-example', 'default'],
   3: ['example', 'default'],
@@ -49,6 +69,22 @@ type ValidationItem = {
   path: JsonPath;
 };
 
+function hasRequiredProperties(schema: traverse.SchemaObject): schema is HasRequiredProperties {
+  return schema.required === undefined || Array.isArray(schema.required);
+}
+
+function isSubpath(path: JsonPath, subPaths: JsonPath[]): boolean {
+  return subPaths.some(subPath => subPath.every((segment, idx) => segment === '*' || segment === path[idx]));
+}
+
+function isMediaRequest(path: JsonPath, oasVersion: 2 | 3): boolean {
+  return isSubpath(path, REQUEST_MEDIA_PATHS[oasVersion]);
+}
+
+function isMediaResponse(path: JsonPath, oasVersion: 2 | 3): boolean {
+  return isSubpath(path, RESPONSE_MEDIA_PATHS[oasVersion]);
+}
+
 function* getMediaValidationItems(
   items: MediaValidationItem[],
   targetVal: Dictionary<unknown>,
@@ -146,6 +182,41 @@ function cleanSchema(schema: Record<string, unknown>): void {
   }));
 }
 
+/**
+ * Modifies 'schema' (and all its sub-schemas) to make all
+ * readOnly or writeOnly properties optional.
+ * In this context, "sub-schemas" refers to all schemas reachable from 'schema'
+ * (e.g. properties, additionalProperties, allOf/anyOf/oneOf, not, items, etc.)
+ * @param schema the schema to be modified
+ * @param readOnlyProperties make readOnly properties optional
+ * @param writeOnlyProperties make writeOnly properties optional
+ */
+function relaxRequired(
+  schema: Record<string, unknown>,
+  readOnlyProperties: boolean,
+  writeOnlyProperties: boolean,
+): void {
+  if (readOnlyProperties || writeOnlyProperties)
+    traverse(schema, {}, <traverse.Callback>((
+      fragment,
+      jsonPtr,
+      rootSchema,
+      parentJsonPtr,
+      parentKeyword,
+      parent,
+      propertyName,
+    ) => {
+      if ((fragment.readOnly === true && readOnlyProperties) || (fragment.writeOnly === true && writeOnlyProperties)) {
+        if (parentKeyword == 'properties' && parent && hasRequiredProperties(parent)) {
+          parent.required = parent.required?.filter(p => p !== propertyName);
+          if (parent.required?.length === 0) {
+            delete parent.required;
+          }
+        }
+      }
+    }));
+}
+
 export default createRulesetFunction<Record<string, unknown>, Options>(
   {
     input: {
@@ -190,6 +261,11 @@ export default createRulesetFunction<Record<string, unknown>, Options>(
     // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
     schemaOpts.schema = JSON.parse(JSON.stringify(schemaOpts.schema));
     cleanSchema(schemaOpts.schema);
+    relaxRequired(
+      schemaOpts.schema,
+      opts.type === 'media' && isMediaRequest(context.path, opts.oasVersion),
+      opts.type === 'media' && isMediaResponse(context.path, opts.oasVersion),
+    );
 
     for (const validationItem of validationItems) {
       const result = oasSchema(validationItem.value, schemaOpts, {


Test patch:
diff --git a/packages/rulesets/src/oas/__tests__/oas2-valid-media-example.test.ts b/packages/rulesets/src/oas/__tests__/oas2-valid-media-example.test.ts
index 0f2954f8..d38a0c70 100644
--- a/packages/rulesets/src/oas/__tests__/oas2-valid-media-example.test.ts
+++ b/packages/rulesets/src/oas/__tests__/oas2-valid-media-example.test.ts
@@ -45,4 +45,70 @@ testRule('oas2-valid-media-example', [
       },
     ],
   },
+
+  {
+    name: 'Ignore required writeOnly parameters on responses',
+    document: {
+      swagger: '2.0',
+      paths: {
+        '/': {
+          post: {
+            responses: {
+              '200': {
+                schema: {
+                  required: ['ro', 'wo'],
+                  properties: {
+                    ro: {
+                      type: 'string',
+                      readOnly: true,
+                    },
+                    wo: {
+                      type: 'string',
+                      writeOnly: true,
+                    },
+                    other: {
+                      type: 'string',
+                    },
+                  },
+                },
+                examples: {
+                  'application/json': {
+                    other: 'foobar',
+                    ro: 'some',
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      responses: {
+        foo: {
+          schema: {
+            required: ['ro', 'wo', 'other'],
+            properties: {
+              ro: {
+                type: 'string',
+                readOnly: true,
+              },
+              wo: {
+                type: 'string',
+                writeOnly: true,
+              },
+              other: {
+                type: 'string',
+              },
+            },
+          },
+          examples: {
+            'application/json': {
+              other: 'foo',
+              ro: 'some',
+            },
+          },
+        },
+      },
+    },
+    errors: [],
+  },
 ]);
diff --git a/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts b/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts
index 4308e983..16521973 100644
--- a/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts
+++ b/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts
@@ -312,6 +312,158 @@ testRule('oas3-valid-media-example', [
     errors: [],
   },
 
+  {
+    name: 'Ignore required readOnly parameters on requests',
+    document: {
+      openapi: '3.0.0',
+      paths: {
+        '/': {
+          post: {
+            requestBody: {
+              content: {
+                'application/json': {
+                  schema: {
+                    required: ['ro', 'wo'],
+                    properties: {
+                      ro: {
+                        type: 'string',
+                        readOnly: true,
+                      },
+                      wo: {
+                        type: 'string',
+                        writeOnly: true,
+                      },
+                      other: {
+                        type: 'string',
+                      },
+                    },
+                  },
+                  example: {
+                    other: 'foobar',
+                    wo: 'some',
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      components: {
+        requestBodies: {
+          foo: {
+            content: {
+              'application/json': {
+                schema: {
+                  required: ['ro', 'wo', 'other'],
+                  properties: {
+                    ro: {
+                      type: 'string',
+                      readOnly: true,
+                    },
+                    wo: {
+                      type: 'string',
+                      writeOnly: true,
+                    },
+                    other: {
+                      type: 'string',
+                    },
+                  },
+                },
+                examples: {
+                  valid: {
+                    summary: 'should be valid',
+                    value: {
+                      other: 'foo',
+                      wo: 'some',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+    errors: [],
+  },
+
+  {
+    name: 'Ignore required writeOnly parameters on responses',
+    document: {
+      openapi: '3.0.0',
+      paths: {
+        '/': {
+          post: {
+            responses: {
+              '200': {
+                content: {
+                  'application/json': {
+                    schema: {
+                      required: ['ro', 'wo'],
+                      properties: {
+                        ro: {
+                          type: 'string',
+                          readOnly: true,
+                        },
+                        wo: {
+                          type: 'string',
+                          writeOnly: true,
+                        },
+                        other: {
+                          type: 'string',
+                        },
+                      },
+                    },
+                    example: {
+                      other: 'foobar',
+                      ro: 'some',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+      components: {
+        responses: {
+          foo: {
+            content: {
+              'application/json': {
+                schema: {
+                  required: ['ro', 'wo', 'other'],
+                  properties: {
+                    ro: {
+                      type: 'string',
+                      readOnly: true,
+                    },
+                    wo: {
+                      type: 'string',
+                      writeOnly: true,
+                    },
+                    other: {
+                      type: 'string',
+                    },
+                  },
+                },
+                examples: {
+                  valid: {
+                    summary: 'should be valid',
+                    value: {
+                      other: 'foo',
+                      ro: 'some',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    },
+    errors: [],
+  },
+
   {
     name: 'parameters: will fail when complex example is used',
     document: {