Skip to content

Commit

Permalink
feat(1-Platform#13): added support for openapiv2
Browse files Browse the repository at this point in the history
  • Loading branch information
akhilmhdh committed Feb 3, 2023
1 parent 60b9bcc commit 70589ed
Show file tree
Hide file tree
Showing 13 changed files with 1,428 additions and 1,200 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ apc:
go run cmd/cli/main.go run

test-json:
go run cmd/cli/main.go run -a openapi --url ./test/petstore-v3.json --config ./test
go run cmd/cli/main.go run -a openapi --url ./test/petstore-v3.json --config ./test

test-v2-openapi:
go run cmd/cli/main.go run -a openapi --url https://petstore.swagger.io/v2/swagger.json --config ./test
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ require (
github.com/dop251/goja v0.0.0-20230128084908-78b980256d04
github.com/getkin/kin-openapi v0.113.0
github.com/goccy/go-json v0.10.0
github.com/invopop/yaml v0.1.0
github.com/pelletier/go-toml/v2 v2.0.6
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand All @@ -20,7 +20,6 @@ require (
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand All @@ -36,4 +35,5 @@ require (
golang.org/x/text v0.5.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwu
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 h1:kgvzE5wLsLa7XKfV85VZl40QXaMCaeFtHpPwJ8fhotY=
github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
github.com/dop251/goja v0.0.0-20230128084908-78b980256d04 h1:iQQgQ1wBsFmpu6OjINCY2ekdknKNNpxO/GOzzww2Amk=
github.com/dop251/goja v0.0.0-20230128084908-78b980256d04/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
Expand Down
10 changes: 2 additions & 8 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/1-platform/api-catalog/internal/cli/filereader"
"github.com/1-platform/api-catalog/internal/cli/pluginmanager"
"github.com/1-platform/api-catalog/internal/cli/reportmanager"
"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -81,13 +80,8 @@ func Run() {
// validation
switch apiType {
case "openapi":
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData(raw)
if err != nil {
log.Fatal(err)
}
if err := doc.Validate(loader.Context); err != nil {
log.Fatal(err)
if err := OpenAPIValidator(raw, apiSchemaFile); err != nil {
log.Fatal("Failed to validate openapi schema/n", err)
}
// iterate over rule
default:
Expand Down
15 changes: 13 additions & 2 deletions internal/cli/filereader/file_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"strings"

"github.com/goccy/go-json"
"github.com/invopop/yaml"
"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
)

var (
Expand Down Expand Up @@ -174,7 +174,18 @@ func (fr *FileReader) ReadFileReturnRaw(location string, data any) ([]byte, erro
return nil, err
}

ext := filepath.Ext(location)[1:] // .json -> json
ext := filepath.Ext(location)
// if file path contains extension it will be parsed ext => .extension
// thus satisfy all local reads
// now if this is a url without extension at end
// for time being i just applied yaml as both json and yaml is supported
// need to find a way to accurately detect
if ext == "" {
ext = "yaml"
} else {
ext = ext[1:] //.json ->json
}

if err := fr.ParseFile(raw, data, ext); err != nil {
return nil, err
}
Expand Down
53 changes: 53 additions & 0 deletions internal/cli/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cli

import (
"strings"

"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
"github.com/invopop/yaml"
)

func OpenAPIValidator(raw []byte, apiSchemaFile map[string]any) error {
if val, ok := apiSchemaFile["swagger"]; ok && (strings.HasPrefix(val.(string), "2.") || val.(string) == "2") {
// convert to OpenAPIv3
var docv2 openapi2.T
if err := yaml.Unmarshal(raw, &docv2); err != nil {
return err
}
docv3, err := openapi2conv.ToV3(&docv2)
if err != nil {
return nil
}

// loader := openapi3.NewLoader()
// if err = docv3.Validate(loader.Context); err != nil {
// return err
// }

// now we need the openapi v3 version of map[strings]
if raw, err = docv3.MarshalJSON(); err != nil {
return err
}

if err = yaml.Unmarshal(raw, &apiSchemaFile); err != nil {
return err
}

return nil
}

loader := openapi3.NewLoader()
docv3, err := loader.LoadFromData(raw)
if err != nil {
return err
}

if err := docv3.Validate(loader.Context); err != nil {
return err
}

return nil

}
8 changes: 6 additions & 2 deletions plugins/builtin/openapi/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ rules:
file: "unsafe_url_character_check.js"
url_length:
file: "url_length.js"
req_body_case_checker:
file: "req_body_case_checker.js"
schema_case_checker:
file: "schema_case_checker.js"
url_similiarity_check:
file: "url_similiarity_check.js"
options:
weight: 0.8
47 changes: 29 additions & 18 deletions plugins/builtin/openapi/status_code_check.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
export default function (config) {
export default function (config, options) {
let numberOfResponses = 0;
let numbnerOfFalseResponses = 0;

const allowedStatusCodes = options?.allowed_status_codes;

Object.keys(config.schema.paths).forEach((path) => {
Object.keys(config.schema.paths[path]).forEach((method) => {
Object.keys(config.schema.paths[path][method].responses).forEach(
(responseStatusCode) => {
numberOfResponses++;
// convert string to number for statuscode
const code = parseInt(responseStatusCode, 10);
if (responseStatusCode !== "default" && Number.isNaN(code)) {
numbnerOfFalseResponses++;
config.report({
message: `Invalid status code - ${responseStatusCode}`,
path: path,
method: method,
});
} else if (
responseStatusCode !== "default" &&
(code < 100 || code > 599)
) {
numbnerOfFalseResponses++;
config.report({
message: `Invalid status code - ${responseStatusCode}`,
path: path,
method: method,
});
if (responseStatusCode !== "default") {
if (Number.isNaN(code)) {
numbnerOfFalseResponses++;
config.report({
message: `Invalid status code - ${responseStatusCode}`,
path: path,
method: method,
});
} else if (code < 100 || code > 599) {
numbnerOfFalseResponses++;
config.report({
message: `Invalid status code - ${responseStatusCode}`,
path: path,
method: method,
});
} else if (
Boolean(allowedStatusCodes) &&
!allowedStatusCodes.includes(responseStatusCode)
) {
numbnerOfFalseResponses++;
config.report({
message: `Statuscode is not allowed - ${responseStatusCode}`,
path: path,
method: method,
});
}
}
}
);
Expand Down
13 changes: 9 additions & 4 deletions plugins/builtin/openapi/url_length.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ function isDynamicPathFragment(path) {
return path[0] === "{" && path[path.length - 1] === "}";
}

export default function (config) {
export default function (config, options) {
let numberOfResponses = 0;
let numbnerOfFalseResponses = 0;

const dynamicPathWeight = options?.weight || 5;
const maxURLAllowedLength = options?.max_url_length || 75;

Object.keys(config.schema.paths).forEach((path) => {
numberOfResponses++;
const resources = path.split("/").filter(Boolean);
Expand All @@ -15,19 +18,21 @@ export default function (config) {
// gives total length
const resourceLength = resources.reduce(
(prev, curr) =>
isDynamicPathFragment(curr) ? prev + 5 : prev + curr.length,
isDynamicPathFragment(curr)
? prev + dynamicPathWeight
: prev + curr.length,
0
);

if (resourceLength > 75 || resources > 10) {
if (resourceLength > maxURLAllowedLength || resources > 10) {
numbnerOfFalseResponses++;

// get all methods
const methods = Object.keys(config.schema.paths[path])
.join(", ")
.toUpperCase();
config.report({
message: `URL is too big, Resources: ${resources} Length: ${resourceLength} Weight: 75`,
message: `URL is too big, Resources: ${resources} Length: ${resourceLength} Weight: ${dynamicPathWeight}`,
path: path,
method: methods,
});
Expand Down
66 changes: 66 additions & 0 deletions plugins/builtin/openapi/url_similiarity_check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Kudos: https://github.com/aceakash/string-similarity
function compareTwoStrings(first, second) {
first = first.replace(/\s+/g, "");
second = second.replace(/\s+/g, "");

if (first === second) return 1; // identical or empty
if (first.length < 2 || second.length < 2) return 0; // if either is a 0-letter or 1-letter string

let firstBigrams = new Map();
for (let i = 0; i < first.length - 1; i++) {
const bigram = first.substring(i, i + 2);
const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1;

firstBigrams.set(bigram, count);
}

let intersectionSize = 0;
for (let i = 0; i < second.length - 1; i++) {
const bigram = second.substring(i, i + 2);
const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0;

if (count > 0) {
firstBigrams.set(bigram, count - 1);
intersectionSize++;
}
}

return (2.0 * intersectionSize) / (first.length + second.length - 2);
}

export default function (config, options) {
let numberOfResponses = 0;
let numbnerOfFalseResponses = 0;
const weight = options?.weight || 0.8;

const paths = Object.keys(config.schema.paths);
for (let i = 0; i < paths.length; i++) {
for (let j = paths.length - 1; j >= i; j--) {
numberOfResponses++;
if (j !== i) {
const similiarity = compareTwoStrings(paths[i], paths[j]);
if (similiarity > weight) {
numbnerOfFalseResponses++;

// get all methods
const methods = Object.keys(config.schema.paths[paths[i]])
.join(", ")
.toUpperCase();

config.report({
message: `URL ${paths[i]} similiar to ${paths[j]}, similiarity: ${similiarity}`,
path: paths[i],
method: methods,
});
}
}
}
}

const score =
(Math.max(numberOfResponses - numbnerOfFalseResponses, 0) /
numberOfResponses) *
100;

config.setScore("quality", score);
}
16 changes: 11 additions & 5 deletions test/apic.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
title = "hello world"

[rules.url_length]
disable = true

[rules.req_body_case_checker.options]
casing = "snakecase"
# [rules.url_length]
# disable = true
#
[rules.url_similiarity_check.options]
weight = 0.90

[plugins.rules.test_plugin]
file = "./test/test_plugin.js"

[plugins.rules.test_plugin.options]
test_data = "hello"

[rules.url_length.options]
weight = 8

# [rules.status_code_check.options]
# allowed_status_codes = [200, 201]
Loading

0 comments on commit 70589ed

Please sign in to comment.