Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable AdditionalProperties:false override #2008

Open
savage-alex opened this issue Dec 21, 2021 · 1 comment
Open

Enable AdditionalProperties:false override #2008

savage-alex opened this issue Dec 21, 2021 · 1 comment
Labels
enhancement New feature or request triaged

Comments

@savage-alex
Copy link

User story.
As an API designer
I want to ensure my examples are correctly spelled and typed for properties that are in my API definition
So that any consumers of mocked endpoints do not get incorrect properties

Is your feature request related to a problem?
Spectral can find examples that are bad when additionalProperties is set to false but its not something we want to do when we release API definitions as its stops evolution

Describe the solution you'd like
A mode for spectral to lint the examples against the definition and to ensure no additionalProperties are present (expect it to be a additional mode)

Additional context
Add any other context or screenshots about the feature request here.

@P0lip P0lip added the enhancement New feature or request label Mar 11, 2022
@derbylock
Copy link

I've solved the same problem by creating custom function base on oasExample:

rules.yaml:

  oas3-valid-schema-example-strict:
    severity: error
    message: "{{error}}"
    recommended: true
    formats: ["oas3"]
    given:
      - "$.components.schemas..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
      - "$..content..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
      - "$..headers..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
      - "$..parameters..[?(@property !== 'properties' && @ && (@ && @.example !== void 0 || @.default !== void 0) && (@.enum || @.type || @.format || @.$ref || @.properties || @.items))]"
    then:
      function: oasExampleStrict
      functionOptions:
        schemaField: "$"
        oasVersion: 3
        type: "schema"
  oas3-valid-media-example-strict:
    severity: error
    message: "{{error}}"
    recommended: true
    formats: ["oas3"]
    given:
      - "$..content..[?(@ && @.schema && (@.example !== void 0 || @.examples))]"
      - "$..headers..[?(@ && @.schema && (@.example !== void 0 || @.examples))]"
      - "$..parameters..[?(@ && @.schema && (@.example !== void 0 || @.examples))]"
    then:
      function: oasExampleStrict
      functionOptions:
        schemaField: "schema"
        oasVersion: 3
        type: "media"

oasExampleStrict.ts

import { isObject } from './isObject';
import type { Dictionary, JsonPath, Optional } from '@stoplight/types';
import oasSchema, { Options as SchemaOptions } from './oasSchema';
import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core';
import { oas2 } from '@stoplight/spectral-formats';
import { apierr } from './sm_common';

export type Options = {
  oasVersion: 2 | 3;
  schemaField: string;
  type: 'media' | 'schema';
};

type MediaValidationItem = {
  field: string;
  multiple: boolean;
  keyed: boolean;
};

const MEDIA_VALIDATION_ITEMS: Dictionary<MediaValidationItem[], 2 | 3> = {
  2: [
    {
      field: 'examples',
      multiple: true,
      keyed: false,
    },
  ],
  3: [
    {
      field: 'example',
      multiple: false,
      keyed: false,
    },
    {
      field: 'examples',
      multiple: true,
      keyed: true,
    },
  ],
};

const SCHEMA_VALIDATION_ITEMS: Dictionary<string[], 2 | 3> = {
  2: ['example', 'x-example', 'default'],
  3: ['example', 'default'],
};

type ValidationItem = {
  value: unknown;
  path: JsonPath;
};

function* getMediaValidationItems(
  items: MediaValidationItem[],
  targetVal: Dictionary<unknown>,
  givenPath: JsonPath,
  oasVersion: 2 | 3,
): Iterable<ValidationItem> {
  for (const { field, keyed, multiple } of items) {
    if (!(field in targetVal)) {
      continue;
    }

    const value = targetVal[field];

    if (multiple) {
      if (!isObject(value)) continue;

      for (const exampleKey of Object.keys(value)) {
        const exampleValue = value[exampleKey];
        if (oasVersion === 3 && keyed && (!isObject(exampleValue) || 'externalValue' in exampleValue)) {
          // should be covered by oas3-examples-value-or-externalValue
          continue;
        }

        const targetPath = [...givenPath, field, exampleKey];

        if (keyed) {
          targetPath.push('value');
        }

        yield {
          value: keyed && isObject(exampleValue) ? exampleValue.value : exampleValue,
          path: targetPath,
        };
      }

      return;
    } else {
      return yield {
        value,
        path: [...givenPath, field],
      };
    }
  }
}

function* getSchemaValidationItems(
  fields: string[],
  targetVal: Record<string, unknown>,
  givenPath: JsonPath,
): Iterable<ValidationItem> {
  for (const field of fields) {
    if (!(field in targetVal)) {
      continue;
    }

    yield {
      value: targetVal[field],
      path: [...givenPath, field],
    };
  }
}

export default createRulesetFunction<Record<string, unknown>, Options>(
  {
    input: {
      type: 'object',
    },
    options: {
      type: 'object',
      properties: {
        oasVersion: {
          enum: [2, 3],
        },
        schemaField: {
          type: 'string',
        },
        type: {
          enum: ['media', 'schema'],
        },
      },
      additionalProperties: false,
    },
  },
  function oasExample(targetVal, opts, context) {
    const formats = context.document.formats;
    const schemaOpts: SchemaOptions = {
      schema: opts.schemaField === '$' ? targetVal : (targetVal[opts.schemaField] as SchemaOptions['schema']),
    };

    let results: Optional<IFunctionResult[]> = void 0;

    const validationItems =
      opts.type === 'schema'
        ? getSchemaValidationItems(SCHEMA_VALIDATION_ITEMS[opts.oasVersion], targetVal, context.path)
        : getMediaValidationItems(MEDIA_VALIDATION_ITEMS[opts.oasVersion], targetVal, context.path, opts.oasVersion);

    schemaOpts.schema = Object.assign({}, schemaOpts.schema);
    disableAdditionalProperties(schemaOpts.schema);
    
    if (formats?.has(oas2) && 'required' in schemaOpts.schema && typeof schemaOpts.schema.required === 'boolean') {
      schemaOpts.schema = { ...schemaOpts.schema };
      delete schemaOpts.schema.required;
    }

    for (const validationItem of validationItems) {
      const result = oasSchema(validationItem.value, schemaOpts, {
        ...context,
        path: validationItem.path,
      });

      if (Array.isArray(result)) {
        if (results === void 0) results = [];
        results.push(...result);
      }
    }
    return results;
  },
);


function disableAdditionalProperties(schema) {
  if (schema.type == "object") {
    schema.additionalProperties = false;
    if (schema.properties && isObject(schema.properties)) {
      schema.properties = Object.assign({}, schema.properties);
      for (const propName in schema.properties) {
        schema.properties[propName] = Object.assign({}, schema.properties[propName]);
        disableAdditionalProperties(schema.properties[propName]);
      }
    }
  }
}

It would be good to have an option in oasExample function and such rules as an optional part of base rules anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request triaged
Projects
None yet
Development

No branches or pull requests

4 participants