Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@snyk/sweater-comb

Package Overview
Dependencies
Maintainers
2
Versions
242
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@snyk/sweater-comb - npm Package Compare versions

Comparing version 0.1.2 to 0.3.1

.releaserc

23

.eslintrc.js
module.exports = {
"env": {
"commonjs": true,
"jest": true,
"node": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
}
env: {
commonjs: true,
jest: true,
node: true,
es2021: true,
},
extends: ['eslint:recommended', 'prettier'],
parserOptions: {
ecmaVersion: 12,
},
rules: {},
};

@@ -1,4 +0,2 @@

const camelCaseRegex = /^([a-z]+([A-Z][a-z]+)*)$/;
module.exports = (targetVal, opts, context) => {
module.exports = (targetVal, opts) => {
const re = new RegExp(opts.match);

@@ -8,2 +6,3 @@ const result = targetVal.filter((item) => {

});
if (!result || result.length === 0) {

@@ -10,0 +9,0 @@ return [

@@ -1,6 +0,4 @@

const camelCaseRegex = /^([a-z]+([A-Z][a-z]+)*)$/;
module.exports = (targetVal, opts, context) => {
for (var i = 0; i < opts.path.length; i++) {
targetVal = targetVal[opts.path[i]];
module.exports = (targetVal, opts) => {
for (let item of opts.path) {
targetVal = targetVal && targetVal[item];
if (!targetVal) {

@@ -7,0 +5,0 @@ break;

{
"name": "@snyk/sweater-comb",
"version": "v0.1.2+202109280930-0ebd495",
"version": "0.3.1",
"description": "Snyk API linting rules",

@@ -30,5 +30,10 @@ "main": "apinext.yaml",

"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jest": "^24.5.2",
"immer": "^9.0.6",
"jest": "^27.0.6",
"pac-resolver": "^5.0.0",
"prettier": "^2.3.2"
}
},
"dependencies": {}
}
# @snyk/sweater-comb
OpenAPI linting rules for Snyk APIs.
Sweats the small stuff, so you don't have to. OpenAPI linting rules for Snyk APIs.
# Usage
# [Intro](docs/intro.md)
At Snyk, we're starting an API program that aims to maximize the value we provide to developers and the extensibility of our platform through our APIs.
Such an API needs some guardrails to stay cohesive, consistent and "unsurprising" to its consumers, as the platform scales in the number of concepts it provides and the number of teams delivering them.
Sweater Comb helps provide some of those guardrails with automation, initially by applying custom [Spectral](https://stoplight.io/open-source/spectral/) linter rules to our OpenAPI specifications.
[Read more about our API program here](docs/intro.md).
# [JSON API: The Good Parts](docs/jsonapi.md)
## What is JSON API?
[JSON API](https://jsonapi.org/) is a standard for representing resources as JSON data.
Generally, our API adheres closely to the [JSON API specification](https://jsonapi.org/format/). [JSON API: The Good Parts](docs/jsonapi.md) describes how we adapted JSON API into our API standards.
## Why build on JSON API?
We found JSON API to be an excellent starting point for a resource-based API, formatting and structuring JSON data in requests and responses. Leveraging JSON API's opinionated choices enabled us to focus more on designing and building the actual content of our API.
## Our JSON API implementation, by example
What does JSON API look like? What do I need to know to get started building a resource in 5 minutes? Let’s cover the basics first; you can always refer to the JSON API specification for a deeper understanding of specific details.
[Read more about our experiences with JSON API here](docs/jsonapi.md).
# [Versioning](docs/version.md)
How we version our API, and more to the point, API requirements necessary in order to implement our versioning scheme.
[Read more about how we version here](docs/version.md).
# [Snyk API Standards](docs/standards.md)
[Everything else; other requirements we found necessary to keep our API nice and neat](docs/standards.md).
# Installation
## NPM

@@ -19,16 +57,1 @@

```
## Other projects
## Coming soon
Integration with [vervet](https://github.com/snyk/vervet), for comprehensive
linting of OpenAPI content at the project level: directory structure, applying
rules to different API conventions appropriately (json:api, RPC, miscellanous
endpoints).
# Contact
[#ask-elk](https://snyk.slack.com/archives/C01HY22DV0F)
const { Spectral } = require('@stoplight/spectral-core');
const { loadRules, loadSpec } = require('./utils');
const { loadRules, loadSpec, getAllErrors } = require('./utils');
let rules;
const openApiDocument = {
openapi: '3.0.3',
info: {
title: 'MyService',
version: '3.0.0',
},
servers: [],
components: {
headers: {},
parameters: {},
responses: {},
schemas: {},
},
paths: {},
};
beforeAll(async () => {
rules = await loadRules('jsonapi.yaml');
});
describe('JSON API Schema', () => {
let spectral;
let rules;
let spec;
it('fails on version schema rules', async () => {
const spectral = new Spectral();
spectral.setRuleset(rules);
const result = await spectral.run(
loadSpec('fixtures/jsonapiSchema.fail.yaml'),
);
expect(result).not.toHaveLength(0);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-get-post-response-data-schema',
message: '"properties" property must have required property "id"',
path: [
'paths',
'/goof/badJsonApi/missingDataId',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
'data',
'properties',
],
}),
expect.objectContaining({
code: 'jsonapi-get-post-response-data-schema',
message: '"data" property must have required property "properties"',
path: [
'paths',
'/goof/badJsonApi/dataIdNotUuid',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
'data',
],
}),
expect.objectContaining({
code: 'jsonapi-get-post-response-data-schema',
message: '"properties.data" property must exist',
path: [
'paths',
'/goof/badJsonApi/dataMissingType',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
],
}),
expect.objectContaining({
code: 'jsonapi-response-jsonapi',
message: 'JSON:API response schema requires jsonapi property',
path: [
'paths',
'/goof/badJsonApi/dataMissingType',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
],
}),
expect.objectContaining({
code: 'jsonapi-get-post-response-data',
message: 'JSON:API response schema requires data property',
path: [
'paths',
'/goof/badJsonApi/dataMissingType',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
],
}),
]),
);
beforeAll(async () => {
rules = await loadRules('jsonapi.yaml');
spectral = new Spectral();
spectral.setRuleset(rules);
spec = loadSpec('fixtures/jsonapiSchema.fail.yaml');
});
describe('Schema rules', () => {
let result;
it('fails on version schema rules', async () => {
// Act
result = await spectral.run(spec);
// Assert
expect(result).not.toHaveLength(0);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-get-post-response-data-schema',
message: '"properties" property must have required property "id"',
path: [
'paths',
'/goof/badJsonApi/missingDataId',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
'data',
'properties',
],
}),
expect.objectContaining({
code: 'jsonapi-get-post-response-data-schema',
message: '"data" property must have required property "properties"',
path: [
'paths',
'/goof/badJsonApi/dataIdNotUuid',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
'data',
],
}),
expect.objectContaining({
code: 'jsonapi-get-post-response-data-schema',
message: '"properties.data" property must exist',
path: [
'paths',
'/goof/badJsonApi/dataMissingType',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
],
}),
expect.objectContaining({
code: 'jsonapi-response-jsonapi',
message: 'JSON:API response schema requires jsonapi property',
path: [
'paths',
'/goof/badJsonApi/dataMissingType',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
],
}),
expect.objectContaining({
code: 'jsonapi-get-post-response-data',
message: 'JSON:API response schema requires data property',
path: [
'paths',
'/goof/badJsonApi/dataMissingType',
'get',
'responses',
'200',
'content',
'application/vnd.api+json',
],
}),
expect.objectContaining({
code: 'jsonapi-post-response-201',
message:
'Post responses must respond with a 201 status code on success',
path: [
'paths',
'/goof/bad_post_status_code',
'post',
'responses',
'200',
],
}),
expect.objectContaining({
code: 'jsonapi-patch-response-data',
message: 'JSON:API patch 200 response requires a schema',
path: [
'paths',
'/goof/patch_missing_content',
'patch',
'responses',
'200',
],
}),
]),
);
});
it('fails if 200 patch response contains invalid properties', async () => {
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-patch-response-200-schema',
message: 'Property "anotherField" is not expected to be here',
path: [
'paths',
'/goof/patch_no_meta_only',
'patch',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
],
}),
]),
);
});
it('fails if 200 patch response does not have meta property', async () => {
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-patch-response-200-schema',
message: '"properties" property must have required property "meta"',
path: [
'paths',
'/goof/patch_no_data',
'patch',
'responses',
'200',
'content',
'application/vnd.api+json',
'schema',
'properties',
],
}),
]),
);
});
it('fails if 204 patch response has content', async () => {
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-patch-response-204-schema',
message: '"content" property must be falsy',
}),
]),
);
});
it('fails if delete response does not use 200 or 204 status code', async () => {
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-delete-response-statuses',
message: 'Delete endpoints can only use 200 or 204 status codes',
}),
]),
);
});
it('fails if 4xx is not 400,401,403,404,409, or 429', async () => {
const result = await spectral.run(spec);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-4xx-response-codes',
message:
'Only 400, 401, 403, 404, 409, and 429 status codes can be returned in the 4xx.',
}),
]),
);
});
it('fails if 200 delete response does not have meta', async () => {
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-delete-response-200',
message: '"properties" property must have required property "meta"',
}),
]),
);
});
it('fails if 204 delete response has content', async () => {
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-delete-response-204',
message: '"content" property must be falsy',
}),
]),
);
});
});
describe('Schema Rules', () => {
let result;
it('fails if a non-204 response has no content', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/no_content': {
get: {
responses: {
200: {},
201: {},
204: {},
},
},
},
};
// Act
result = await spectral.run(spec);
// Assert
const errorArray = getAllErrors(result, 'jsonapi-content-non-204');
expect(errorArray).toHaveLength(2);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-content-non-204',
message: 'Responses from non-204 statuses must have content',
}),
]),
);
});
});
describe('Self linking rules', () => {
it('fails if a GET 200 response does not have a top-level self link', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/example': {
get: {
responses: {
200: {
content: {
'application/vnd.api+json': {
schema: {
type: 'object',
properties: {
data: {},
jsonapi: {},
},
},
},
},
},
},
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
const errorArray = getAllErrors(result, 'jsonapi-self-links-get-patch');
expect(errorArray).toHaveLength(1);
});
it('fails if a PATCH 200 response does not have a top-level self link', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/example': {
patch: {
parameters: [],
responses: {
200: {
content: {
'application/vnd.api+json': {
schema: {
type: 'object',
properties: {
meta: {},
},
required: ['meta'],
additionalProperties: false,
},
},
},
},
},
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
const errorArray = getAllErrors(result, 'jsonapi-self-links-get-patch');
expect(errorArray).toHaveLength(1);
});
it('fails if a POST 201 response does not have a top-level self link', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/example': {
post: {
parameters: [],
responses: {
201: {
content: {
'application/vnd.api+json': {
schema: {
type: 'object',
properties: {
jsonapi: {},
data: {},
},
required: ['jsonapi', 'data'],
additionalProperties: false,
},
},
},
},
},
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
const errorArray = getAllErrors(result, 'jsonapi-self-links-post');
expect(errorArray).toHaveLength(1);
});
});
describe('Collection Rules', () => {
it('fails if collection requests do not include pagination parameters', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/collection_pagination_parameters': {
get: {
parameters: [
{
description:
'Return the page of results immediately after this cursor',
in: 'query',
name: 'starting_after',
schema: {
type: 'string',
},
},
],
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-pagination-collection-parameters',
message: 'Collection requests must include pagination parameters',
}),
]),
);
});
it('fails if collection responses do not include pagination links', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/collection_pagination_links': {
get: {
responses: {
200: {
content: {
'application/vnd.api+json': {
schema: {
type: 'object',
properties: {
jsonapi: {
$ref: '#/components/schemas/JsonApi',
},
},
},
},
},
},
},
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-pagination-collection-links',
message:
'Responses for collection requests must include pagination links',
}),
]),
);
});
it('fails if non-GET for collections requests include pagination parameters', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.components.parameters = {
StartingAfter: {
description:
'Return the page of results immediately after this cursor',
in: 'query',
name: 'starting_after',
schema: {
type: 'string',
},
},
};
spec.paths = {
'/org/{org_id}/non_get_pagination_parameters': {
post: {
parameters: [
{
$ref: '#/components/parameters/Version',
},
{
$ref: '#/components/parameters/StartingAfter',
},
],
},
patch: {
parameters: [
{
$ref: '#/components/parameters/Version',
},
{
$ref: '#/components/parameters/StartingAfter',
},
],
},
delete: {
parameters: [
{
$ref: '#/components/parameters/Version',
},
{
$ref: '#/components/parameters/StartingAfter',
},
],
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'jsonapi-no-pagination-parameters',
message: 'Non-GET requests should not allow pagination parameters',
}),
]),
);
});
});
describe('Relationships', () => {
it('passes with correct relationship schema', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/relationship': {
get: {
responses: {
200: {
content: {
'application/vnd.api+json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
type: {
type: 'string',
},
id: {
type: 'string',
},
attributes: {},
relationships: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
data: {
properties: {
type: {
type: 'string',
},
id: {
type: 'string',
format: 'uuid',
},
},
required: ['type', 'id'],
},
links: {
type: 'object',
properties: {
related: {
type: 'string',
},
},
},
},
required: ['data'],
},
},
},
},
},
},
},
},
},
},
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
const errors = getAllErrors(
result,
'jsonapi-response-relationship-schema',
);
expect(errors).toHaveLength(0);
});
it('fails on incorrect relationship schema', async () => {
// Arrange
const spec = {
...openApiDocument,
};
spec.paths = {
'/org/{org_id}/relationship': {
get: {
responses: {
200: {
content: {
'application/vnd.api+json': {
schema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
type: {
type: 'string',
},
id: {
type: 'string',
},
attributes: {},
relationships: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
data: {
properties: {
type: {
type: 'string',
},
id: {
type: 'string',
format: 'uuid',
},
},
required: ['type', 'id'],
},
},
required: ['data'],
},
},
},
},
},
},
},
},
},
},
},
},
};
// Act
const result = await spectral.run(spec);
// Assert
const errors = getAllErrors(
result,
'jsonapi-response-relationship-schema',
);
expect(errors).toHaveLength(1);
});
});
});

@@ -25,5 +25,12 @@ const path = require('path');

function getAllErrors(errors, errorCode = '') {
if (!Array.isArray(errors)) return [];
return errors.filter((item) => item.code && item.code === errorCode);
}
module.exports = {
loadRules,
loadSpec,
getAllErrors,
};

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc