├── .gitignore
├── openapi3edit
├── examples
│ └── operations
│ │ └── main.go
├── null.go
├── spec_edit.go
├── paths_test.go
├── required.go
├── deprecated.go
├── operations_references_fix.go
├── README.md
├── schemas_keys.go
├── delete.go
├── multi.go
└── schemas_flatten.go
├── docs
├── images
│ ├── logo_swaggman_600x150.png
│ ├── logo_swaggman_600x150.psd
│ ├── logo_swaggman_4000x1000.png
│ ├── logo_swaggman_4000x1000.psd
│ └── icon_green-hat.svg
├── index.md
├── openapi3
│ └── inspect
│ │ └── missing_descriptions.md
├── openapi3lint
│ └── custom_rules.md
├── openapi3_fix.md
└── recommendations_openapi3.md
├── cmd
├── oas3lint
│ ├── policy_int-have-std-format.json
│ ├── policy_api_ringcentral.json
│ └── main.go
├── oas2meta
│ └── main.go
├── openapi2csv
│ └── main.go
├── openapi2postman
│ └── main.go
├── openapi2to3
│ └── main.go
├── spectrum
│ └── main.go
└── oas3validate
│ └── main.go
├── openapi3
├── springopenapi3
│ ├── README.md
│ └── spring_parser_test.go
├── content.go
├── copy.go
├── openapi3_url.go
├── path_test.go
├── extensions
│ └── formats.go
├── components.go
├── schemas_test.go
├── constants.go
├── json_pointer.go
├── ontology.go
├── path_more.go
├── inspect.go
├── spec_table_response_status.go
├── extension_props.go
├── json_pointer_test.go
├── openapi3postman2
│ ├── configuration.go
│ └── folders.go
├── merge_options.go
├── intersection.go
├── path.go
├── spec_meta.go
├── ontology
│ └── tag_ontology_test.go
├── schemas.go
├── openapi3html
│ ├── pageparams.go
│ ├── openapi3html
│ │ └── page.qtpl
│ └── apiregistry
│ │ └── main.go
├── visit.go
├── parameter.go
└── read.go
├── examples
├── openapi3table
│ ├── README.md
│ ├── example_simple.html
│ └── main.go
├── ringcentral-engage-digital
│ ├── swaggman.cfg.json
│ ├── ringcentral-engage-digital.postman2.base.json
│ └── convert.go
└── ringcentral
│ ├── convert.go
│ └── ringcentral.postman2.base.json
├── .github
├── dependabot.yaml
└── workflows
│ ├── lint.yaml
│ └── ci.yaml
├── endpoint.go
├── openapi3lint
├── rule_collection.go
├── rule_empty.go
├── rule_example.go
├── lintutil
│ ├── scope.go
│ └── rules.go
├── rule_collection_simple.go
├── ruleopsummaryexist
│ └── rule_operation_summary_exist.go
├── extensions
│ ├── rule_collection.go
│ └── ruletaggroupexist
│ │ └── rule_tag_group_exist.go
├── ruleintstdformat
│ └── rule_datatype_int_format_standard_exist.go
├── ruleopsummarystylefirstuppercase
│ └── rule_operation_summary_first_uppercase.go
├── ruletagstylefirstuppercase
│ └── rule_tag_style_first_uppercase.go
├── ruleschemareferences
│ └── rule_schema_references_exist.go
├── ruleopidstyle
│ └── rule_operation_operationid_style_camelcase.go
├── README.md
├── ruleschemapropenumstyle
│ └── rule_schema_prop_enum_style.go
├── ruleopxpropertystringexist
│ └── rule_op_xproperty_string_exist.go
├── ruleschemaobjectpropsexist
│ └── rule_schema_object_props_exist.go
└── rulepathparamstyle
│ └── rule_path_param_style.go
├── mkdocs.yml
├── ext
├── taxonomy
│ ├── taxonomy.go
│ └── categories.go
└── taggroups
│ └── taggroups.go
├── openapi2
├── spec_meta.go
├── convert.go
├── openapi2openapi3
│ └── convert.go
├── read.go
├── spec_more.go
├── openapi2csv
│ └── spec_to_csv.go
├── copy.go
├── merge.go
└── count.go
├── spec_meta.go
├── .golangci.yaml
├── postman2
├── request.go
├── headers_util.go
├── simple
│ └── postman_collection_v2_simple.go
├── collection.go
└── url.go
├── LICENSE
├── go.mod
└── raml08
└── raml08openapi3
└── operations.go
/.gitignore:
--------------------------------------------------------------------------------
1 | _*
2 | ~$*
3 | .DS_Store
4 | .idea
5 | vendor
--------------------------------------------------------------------------------
/openapi3edit/examples/operations/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func main() {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/docs/images/logo_swaggman_600x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grokify/spectrum/HEAD/docs/images/logo_swaggman_600x150.png
--------------------------------------------------------------------------------
/docs/images/logo_swaggman_600x150.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grokify/spectrum/HEAD/docs/images/logo_swaggman_600x150.psd
--------------------------------------------------------------------------------
/docs/images/logo_swaggman_4000x1000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grokify/spectrum/HEAD/docs/images/logo_swaggman_4000x1000.png
--------------------------------------------------------------------------------
/docs/images/logo_swaggman_4000x1000.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grokify/spectrum/HEAD/docs/images/logo_swaggman_4000x1000.psd
--------------------------------------------------------------------------------
/cmd/oas3lint/policy_int-have-std-format.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules":{
3 | "datatype-int-format-int32-int64": {
4 | "severity": "error"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/openapi3/springopenapi3/README.md:
--------------------------------------------------------------------------------
1 | # From Spring
2 |
3 | This package provides some code useful for converting Spring Java code to OpenAPI 3.0. At this moment it only converts Spring model classes to OpenAPI 3.0 schema objects.
--------------------------------------------------------------------------------
/examples/openapi3table/README.md:
--------------------------------------------------------------------------------
1 | # Create Table
2 |
3 | 1. Download RingCentral OAS2 YAML spec
4 | 1. Convert to JSON using https://github.com/bronze1man/yaml2json
5 | 1. Convert to OAS3 YAML using https://mermade.org.uk/api/v1/convert
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: /
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: github-actions
8 | directory: /
9 | schedule:
10 | interval: daily
11 |
--------------------------------------------------------------------------------
/endpoint.go:
--------------------------------------------------------------------------------
1 | package spectrum
2 |
3 | import "strings"
4 |
5 | func EndpointString(method, path string) string {
6 | method = strings.ToUpper(strings.TrimSpace(method))
7 | path = strings.TrimSpace(path)
8 | return strings.Join([]string{method, path}, " ")
9 | }
10 |
--------------------------------------------------------------------------------
/openapi3lint/rule_collection.go:
--------------------------------------------------------------------------------
1 | package openapi3lint
2 |
3 | type RuleCollections []RuleCollection
4 |
5 | type RuleCollection interface {
6 | Name() string
7 | RuleNames() []string
8 | RuleExists(ruleName string) bool
9 | Rule(ruleName string) (Rule, error)
10 | }
11 |
--------------------------------------------------------------------------------
/examples/ringcentral-engage-digital/swaggman.cfg.json:
--------------------------------------------------------------------------------
1 | {
2 | "useXTagGroups": true ,
3 | "postmanServerUrlApiBasePath": "1.0",
4 | "postmanServerUrl": "{{RINGCENTRAL_ENGAGE_SERVER_URL}}",
5 | "postmanHeaders": [
6 | {
7 | "key": "Authorization",
8 | "value": "Bearer {{RINGCENTRAL_ENGAGE_ACCESS_TOKEN}}"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: lint
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | lint:
6 | strategy:
7 | matrix:
8 | go-version: [1.x]
9 | platform: [ubuntu-latest]
10 | runs-on: ${{ matrix.platform }}
11 | steps:
12 | - uses: actions/checkout@v6.0.1
13 | - name: golangci-lint
14 | uses: golangci/golangci-lint-action@v9.2.0
15 | with:
16 | version: latest
17 | args: --timeout 3m --verbose
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Spectrum - OpenAPI Spec Utility
2 |
3 | Spectrum is a multi-purpose OpenAPI speec utility.
4 |
5 | It can perform the following:
6 |
7 | OpenAPI Spec 3
8 |
9 | * Convert to Postman Collection v2
10 | * Merge Multiple OAS3 Specs
11 | * Validate OAS3 Specs
12 | * Programmatically examine and modify OAS3 Specs
13 | * Programmatically fix OAS3 Specs
14 |
15 | OpenAPI Spec 2
16 |
17 | * Convert to Postman Collection v2
18 | * Merge Multiple OAS2 Specs
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Spectrum
2 | repo_url: https://github.com/grokify/spectrum
3 | edit_uri: tree/master/docs
4 | docs_dir: docs
5 | theme: readthedocs
6 |
7 | markdown_extensions:
8 | - codehilite:
9 | linenums: "None"
10 |
11 | nav:
12 | - Home: index.md
13 | - OpenAPI3Edit:
14 | - Missing Descriptions: openapi3/inspect/missing_descriptions.md
15 | - OpenAPI3Lint:
16 | - Overview: openapi3lint.md
17 | - Custom Rules: openapi3lint/custom_rules.md
18 |
--------------------------------------------------------------------------------
/openapi3/content.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | oas3 "github.com/getkin/kin-openapi/openapi3"
5 | )
6 |
7 | func ContentToSchemaRefMap(content oas3.Content) map[string]string {
8 | mss := map[string]string{}
9 | // type oas3.Content map[string]*MediaType
10 | for ct, mediaType := range content {
11 | if mediaType.Schema != nil && len(mediaType.Schema.Ref) > 0 {
12 | mss[ct] = mediaType.Schema.Ref
13 | } else {
14 | mss[ct] = ""
15 | }
16 | }
17 | return mss
18 | }
19 |
--------------------------------------------------------------------------------
/cmd/oas2meta/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log/slog"
6 | "os"
7 |
8 | "github.com/grokify/mogo/fmt/fmtutil"
9 | "github.com/grokify/spectrum/openapi2"
10 | )
11 |
12 | func main() {
13 | if len(os.Args) < 2 {
14 | slog.Error("usage: oas2meta ...")
15 | os.Exit(1)
16 | } else if set, err := openapi2.NewSpecMetaSetFilepaths(os.Args[1:]); err != nil {
17 | slog.Error(err.Error())
18 | os.Exit(2)
19 | } else {
20 | fmtutil.MustPrintJSON(set)
21 | fmt.Println("DONE")
22 | os.Exit(0)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ext/taxonomy/taxonomy.go:
--------------------------------------------------------------------------------
1 | package taxonomy
2 |
3 | import (
4 | "github.com/grokify/spectrum/openapi3"
5 | "github.com/grokify/spectrum/openapi3edit"
6 | )
7 |
8 | type Taxonomy struct {
9 | Category CategoryRef `json:"category"`
10 | Slug string `json:"slug"`
11 | }
12 |
13 | func (tax *Taxonomy) AddToSpec(spec *openapi3.Spec) {
14 | if spec != nil {
15 | se := openapi3edit.SpecEdit{}
16 | se.SpecSet(spec)
17 | se.ExtensionSet(XTaxonomy, tax)
18 | }
19 | }
20 |
21 | type CategoryRef struct {
22 | Ref string `json:"$ref"`
23 | }
24 |
--------------------------------------------------------------------------------
/openapi3edit/null.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import oas3 "github.com/getkin/kin-openapi/openapi3"
4 |
5 | // PathsNullToEmpty converts a `path` property from `null` to
6 | // an empty set `{}` to satisfy OpenAPI Generator which will
7 | // fail on the following error "-attribute paths is not of type `object`"
8 | func (se *SpecEdit) PathsNullToEmpty() {
9 | if se.SpecMore.Spec != nil && se.SpecMore.Spec.Paths == nil {
10 | se.SpecMore.Spec.Paths = oas3.NewPaths()
11 | // se.SpecMore.Spec.Paths = map[string]*oas3.PathItem{} // getkin v0.121.0 to v0.122.0
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/openapi3/copy.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "encoding/json"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | )
8 |
9 | func CopySchemaStandard(schema oas3.Schema) (oas3.Schema, error) {
10 | bytes, err := json.Marshal(schema)
11 | if err != nil {
12 | return oas3.Schema{}, err
13 | }
14 | var newSchema oas3.Schema
15 | err = json.Unmarshal(bytes, &newSchema)
16 | if err != nil {
17 | return oas3.Schema{}, err
18 | }
19 | // newSchema.ExtensionProps = oas3.ExtensionProps{}
20 | newSchema.Extensions = map[string]any{}
21 | return newSchema, nil
22 | }
23 |
--------------------------------------------------------------------------------
/openapi3/openapi3_url.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/grokify/mogo/net/urlutil"
7 | )
8 |
9 | func BuildAPIURLOAS(specServerURL, overrideServerURL, specPath string) string {
10 | overrideServerURL = strings.TrimSpace(overrideServerURL)
11 | specServerURL = strings.TrimSpace(specServerURL)
12 | specPath = strings.TrimSpace(specPath)
13 | serverURL := specServerURL
14 | if len(overrideServerURL) > 0 {
15 | serverURL = overrideServerURL
16 | }
17 | return urlutil.CondenseURI(
18 | strings.Join([]string{serverURL, specPath}, "/"))
19 | }
20 |
--------------------------------------------------------------------------------
/openapi3edit/spec_edit.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import "github.com/grokify/spectrum/openapi3"
4 |
5 | type SpecEdit struct {
6 | SpecMore openapi3.SpecMore
7 | }
8 |
9 | func NewSpecEdit(spec *openapi3.Spec) SpecEdit {
10 | return SpecEdit{
11 | SpecMore: openapi3.SpecMore{Spec: spec}}
12 | }
13 |
14 | func (se *SpecEdit) ExtensionSet(key string, val any) {
15 | // se.SpecMore.Spec.ExtensionProps.Extensions[key] = val
16 | se.SpecMore.Spec.Extensions[key] = val
17 | }
18 |
19 | func (se *SpecEdit) SpecSet(spec *openapi3.Spec) {
20 | se.SpecMore = openapi3.SpecMore{Spec: spec}
21 | }
22 |
--------------------------------------------------------------------------------
/openapi3/path_test.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | var pathVarsToGenericTests = []struct {
8 | v string
9 | want string
10 | }{
11 | {"/user/{userId}", "/user/{}"},
12 | {"/user/{userId}/email/{emailId}", "/user/{}/email/{}"},
13 | }
14 |
15 | // TestDMYHM2ParseTime ensures timeutil.DateDMYHM2 is parsed to GMT timezone.
16 | func TestPathVarsToGeneric(t *testing.T) {
17 | for _, tt := range pathVarsToGenericTests {
18 | got := PathVarsToGeneric(tt.v)
19 | if got != tt.want {
20 | t.Errorf("modify.PathVarsToGeneric(\"%v\") Mismatch: want [%v], got [%v]", tt.v, tt.want, got)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/openapi3edit/paths_test.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | /*
4 | import (
5 | "testing"
6 | )
7 |
8 | var pathVarsToGenericTests = []struct {
9 | v string
10 | want string
11 | }{
12 | {"/user/{userId}", "/user/{}"},
13 | {"/user/{userId}/email/{emailId}", "/user/{}/email/{}"},
14 | }
15 |
16 | // TestDMYHM2ParseTime ensures timeutil.DateDMYHM2 is parsed to GMT timezone.
17 | func TestPathVarsToGeneric(t *testing.T) {
18 | for _, tt := range pathVarsToGenericTests {
19 | got := PathVarsToGeneric(tt.v)
20 | if got != tt.want {
21 | t.Errorf("modify.PathVarsToGeneric(\"%v\") Mismatch: want [%v], got [%v]", tt.v, tt.want, got)
22 | }
23 | }
24 | }
25 | */
26 |
--------------------------------------------------------------------------------
/openapi2/spec_meta.go:
--------------------------------------------------------------------------------
1 | package openapi2
2 |
3 | import (
4 | "github.com/grokify/spectrum"
5 | )
6 |
7 | func NewSpecMetaFilepath(filepath string) (*spectrum.SpecMeta, error) {
8 | if s, err := ReadOpenAPI2KinSpecFile(filepath); err != nil {
9 | return nil, err
10 | } else {
11 | sm := SpecMore{Spec: s}
12 | return sm.Meta(), nil
13 | }
14 | }
15 |
16 | func NewSpecMetaSetFilepaths(filepaths []string) (*spectrum.SpecMetaSet, error) {
17 | set := spectrum.NewSpecMetaSet()
18 | for _, fp := range filepaths {
19 | if m, err := NewSpecMetaFilepath(fp); err != nil {
20 | return nil, err
21 | } else {
22 | set.Data[fp] = *m
23 | }
24 | }
25 | return set, nil
26 | }
27 |
--------------------------------------------------------------------------------
/openapi3/extensions/formats.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import "fmt"
4 |
5 | type Format struct {
6 | StandardType string
7 | XFormat string
8 | StandardFormat string
9 | }
10 |
11 | func Formats() map[string]Format {
12 | data := map[string]Format{
13 | "uint16": {
14 | XFormat: "uint16",
15 | StandardType: "string",
16 | StandardFormat: "int32",
17 | },
18 | "uint32": {
19 | XFormat: "uint32",
20 | StandardType: "string",
21 | StandardFormat: "int64",
22 | },
23 | }
24 | for k, v := range data {
25 | if k != v.XFormat {
26 | panic(fmt.Sprintf("mismatch [%s] [%s]", k, v.XFormat))
27 | }
28 | }
29 | return data
30 | }
31 |
--------------------------------------------------------------------------------
/openapi3/components.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "strings"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | )
8 |
9 | func (sm *SpecMore) ComponentRequestBody(componentPath string) *oas3.RequestBodyRef {
10 | componentPathParts := strings.Split(strings.TrimSpace(componentPath), "/")
11 | if len(componentPathParts) != 4 ||
12 | componentPathParts[0] != "#" ||
13 | componentPathParts[1] != "components" ||
14 | componentPathParts[2] != "requestBodies" ||
15 | len(componentPathParts[3]) == 0 {
16 | return nil
17 | }
18 | if reqBodyRef, ok := sm.Spec.Components.RequestBodies[componentPathParts[3]]; ok {
19 | return reqBodyRef
20 | }
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - master
9 | workflow_dispatch:
10 | jobs:
11 | test:
12 | strategy:
13 | matrix:
14 | go-version: [1.25.x, 1.24.x]
15 | platform: [ubuntu-latest, macos-latest, windows-latest]
16 | runs-on: ${{ matrix.platform }}
17 | steps:
18 | - name: Install Go
19 | if: success()
20 | uses: actions/setup-go@v6
21 | with:
22 | go-version: ${{ matrix.go-version }}
23 | - name: Checkout code
24 | uses: actions/checkout@v6.0.1
25 | - name: Run tests
26 | run: go test -v -covermode=count ./...
27 |
--------------------------------------------------------------------------------
/openapi3lint/rule_empty.go:
--------------------------------------------------------------------------------
1 | package openapi3lint
2 |
3 | import (
4 | oas3 "github.com/getkin/kin-openapi/openapi3"
5 | "github.com/grokify/spectrum/openapi3"
6 | "github.com/grokify/spectrum/openapi3lint/lintutil"
7 | )
8 |
9 | type EmptyRule struct{}
10 |
11 | func (rule EmptyRule) Name() string { return "" }
12 | func (rule EmptyRule) Scope() string { return "" }
13 | func (rule EmptyRule) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
14 | return []lintutil.PolicyViolation{}
15 | }
16 | func (rule EmptyRule) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
17 | return []lintutil.PolicyViolation{}
18 | }
19 |
--------------------------------------------------------------------------------
/examples/ringcentral-engage-digital/ringcentral-engage-digital.postman2.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "variables": [],
3 | "info": {
4 | "name": "RingCentral Engage Digital REST API",
5 | "description": "This is a collection of API requests associated with the Basic usage plan in RingCentral Connect Platform for Sandbox Testing.\n\nPlease use a Postman environment with the following variables: RC_SERVER_HOSTNAME, RC_APP_KEY, RC_APP_SECRET, RC_USERNAME, RC_EXTENSION, RC_PASSWORD\n\nTo use these resources, you must ALWAYS start by executing one of the OAuth \"Auth\" Requests first, then you can use any other requests (for the 60 minutes that the access_token you obtained is valid).",
6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7 | }
8 | }
--------------------------------------------------------------------------------
/openapi3lint/rule_example.go:
--------------------------------------------------------------------------------
1 | package openapi3lint
2 |
3 | import (
4 | "github.com/grokify/mogo/log/severity"
5 | "github.com/grokify/spectrum/openapi3lint/lintutil"
6 | )
7 |
8 | func RulesConfigExample1() map[string]RuleConfig {
9 | rules := map[string]RuleConfig{
10 | lintutil.RulenameOpIDStyleCamelCase: {},
11 | lintutil.RulenameOpSummaryExist: {},
12 | lintutil.RulenameOpSummaryStyleFirstUpperCase: {},
13 | lintutil.RulenamePathParamStylePascalCase: {},
14 | lintutil.RulenameSchemaObjectPropsExist: {},
15 | lintutil.RulenameSchemaPropEnumStylePascalCase: {},
16 | lintutil.RulenameTagStyleFirstUpperCase: {},
17 | }
18 | for name, cfg := range rules {
19 | cfg.Severity = severity.SeverityError
20 | rules[name] = cfg
21 | }
22 | return rules
23 | }
24 |
--------------------------------------------------------------------------------
/openapi3/schemas_test.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | var schemaPointerExpandTests = []struct {
8 | prefix string
9 | schemaName string
10 | want string
11 | }{
12 | {"", "FooBar", "#/components/schemas/FooBar"},
13 | {"spec.json", "FooBar", "spec.json#/components/schemas/FooBar"},
14 | {"spec.json", "#/components/schemas/FooBar", "spec.json#/components/schemas/FooBar"},
15 | }
16 |
17 | // TestDMYHM2ParseTime ensures timeutil.DateDMYHM2 is parsed to GMT timezone.
18 | func TestSchemaPointerExpand(t *testing.T) {
19 | for _, tt := range schemaPointerExpandTests {
20 | got := SchemaPointerExpand(tt.prefix, tt.schemaName)
21 | if got != tt.want {
22 | t.Errorf("openapi3.SchemaPointerExpand(\"%s\",\"%s\") Mismatch: want [%v], got [%v]",
23 | tt.prefix, tt.schemaName, tt.want, got)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/openapi3/constants.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | const (
4 | OASVersionLatest = "3.1.0"
5 | OASVersionDefault = "3.0.3"
6 | apiVersionDefault = "0.0.1"
7 |
8 | TypeArray = "array"
9 | TypeBoolean = "boolean"
10 | TypeInteger = "integer"
11 | TypeObject = "object"
12 | TypeString = "string"
13 | FormatDate = "date"
14 | FormatDateTime = "date-time"
15 | FormatInt32 = "int32"
16 | FormatInt64 = "int64"
17 |
18 | PropertyOperationID = "operationId"
19 | PropertySummary = "summary"
20 | PropertyTags = "tags"
21 |
22 | InCookie = "cookie"
23 | InHeader = "header"
24 | InPath = "path"
25 | InQuery = "query"
26 |
27 | PointerComponentsRequestBodies = "#/components/requestBodies"
28 | PointerComponentsSchemas = "#/components/schemas"
29 | PointerComponentsSchemasFormat = `#/components/schemas/%s`
30 | )
31 |
--------------------------------------------------------------------------------
/spec_meta.go:
--------------------------------------------------------------------------------
1 | package spectrum
2 |
3 | type SpecMeta struct {
4 | Counts SpecMetaCounts
5 | Names SpecMetaNames
6 | }
7 |
8 | func NewSpecMeta() *SpecMeta {
9 | return &SpecMeta{
10 | Counts: SpecMetaCounts{},
11 | Names: SpecMetaNames{}}
12 | }
13 |
14 | func (m *SpecMeta) Inflate() {
15 | m.Counts = m.Names.Counts()
16 | }
17 |
18 | type SpecMetaCounts struct {
19 | Endpoints int
20 | Paths int
21 | Models int
22 | }
23 |
24 | type SpecMetaNames struct {
25 | Endpoints []string
26 | Models []string
27 | Paths []string
28 | }
29 |
30 | func (n SpecMetaNames) Counts() SpecMetaCounts {
31 | return SpecMetaCounts{
32 | Endpoints: len(n.Endpoints),
33 | Models: len(n.Models),
34 | Paths: len(n.Paths),
35 | }
36 | }
37 |
38 | type SpecMetaSet struct {
39 | Data map[string]SpecMeta
40 | }
41 |
42 | func NewSpecMetaSet() *SpecMetaSet {
43 | return &SpecMetaSet{Data: map[string]SpecMeta{}}
44 | }
45 |
--------------------------------------------------------------------------------
/openapi3/json_pointer.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "github.com/grokify/mogo/encoding/jsonpointer"
5 | )
6 |
7 | type JSONPointer jsonpointer.JSONPointer
8 |
9 | func ParseJSONPointer(s string) (JSONPointer, error) {
10 | ptr, err := jsonpointer.ParseJSONPointer(s)
11 | return JSONPointer(ptr), err
12 | }
13 |
14 | const (
15 | PathComponents = "components"
16 | PathParameters = "parameters"
17 | PathPath = "path"
18 | PathSchemas = "schemas"
19 |
20 | PathComponentsParameters = "#/components/parameters"
21 | )
22 |
23 | func (p *JSONPointer) IsTopParameter() (string, bool) {
24 | if len(p.Path) == 3 && p.Path[0] == PathComponents && p.Path[1] == PathParameters {
25 | return p.Path[2], true
26 | }
27 | return "", false
28 | }
29 |
30 | func (p *JSONPointer) IsTopSchema() (string, bool) {
31 | if len(p.Path) == 3 && p.Path[0] == PathComponents && p.Path[1] == PathSchemas {
32 | return p.Path[2], true
33 | }
34 | return "", false
35 | }
36 |
--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | enable:
4 | - dogsled
5 | - dupl
6 | - gosec
7 | - misspell
8 | - nakedret
9 | - staticcheck
10 | - unconvert
11 | - unparam
12 | - whitespace
13 | settings:
14 | staticcheck:
15 | checks:
16 | - -ST1003
17 | - all
18 | exclusions:
19 | generated: lax
20 | presets:
21 | - comments
22 | - common-false-positives
23 | - legacy
24 | - std-error-handling
25 | rules:
26 | - path: openapi2/copy.go
27 | text: '`copyOrIgnoreEndpoint` - `path` is unused'
28 | - path: openapi2/copy.go
29 | text: '`copyOrIgnoreEndpoint` - `specOld` is unused'
30 | paths:
31 | - third_party$
32 | - builtin$
33 | - examples$
34 | formatters:
35 | enable:
36 | - gofmt
37 | - goimports
38 | exclusions:
39 | generated: lax
40 | paths:
41 | - third_party$
42 | - builtin$
43 | - examples$
44 |
--------------------------------------------------------------------------------
/openapi3/ontology.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import oas3 "github.com/getkin/kin-openapi/openapi3"
4 |
5 | // OntologySet is a set of ontologies which can be used to understand
6 | // ontologies by a key, such as filename or by tag.
7 | type OntologySet struct {
8 | Ontologies map[string]Ontology `json:"ontologies"`
9 | }
10 |
11 | // Ontology returns the naming structure of an OpenAPI Spec. It is useful
12 | // for understanding the naming conventions of an existing OpenAPI Spec.
13 | // For example, the relationship of operationIDs to paths and the relationship
14 | // of parameter name component keys to parameter names.
15 | type Ontology struct {
16 | Operations map[string]OperationMeta `json:"operationIDs"`
17 | SchemaNames []string `json:"schemaNames"`
18 | Parameters oas3.ParametersMap `json:"parameters"`
19 | }
20 |
21 | func NewOntology() Ontology {
22 | return Ontology{
23 | Operations: map[string]OperationMeta{},
24 | SchemaNames: []string{},
25 | Parameters: oas3.ParametersMap{}}
26 | }
27 |
--------------------------------------------------------------------------------
/postman2/request.go:
--------------------------------------------------------------------------------
1 | package postman2
2 |
3 | type Request struct {
4 | URL *URL `json:"url,omitempty"`
5 | Method string `json:"method,omitempty"`
6 | Header []Header `json:"header,omitempty"`
7 | Body *RequestBody `json:"body,omitempty"`
8 | Description string `json:"description,omitempty"`
9 | }
10 |
11 | type Header struct {
12 | Key string `json:"key,omitempty"`
13 | Value string `json:"value,omitempty"`
14 | Description string `json:"description,omitempty"`
15 | }
16 |
17 | type RequestBody struct {
18 | Mode string `json:"mode,omitempty"` // `raw`, `urlencoded`, `formdata`,`file`,`graphql`
19 | Raw string `json:"raw,omitempty"`
20 | URLEncoded []URLEncodedParam `json:"urlencoded,omitempty"`
21 | }
22 |
23 | type URLEncodedParam struct {
24 | Key string `json:"key,omitempty"`
25 | Value string `json:"value,omitempty"`
26 | Type string `json:"type,omitempty"`
27 | Enabled bool `json:"enabled,omitempty"`
28 | }
29 |
--------------------------------------------------------------------------------
/openapi2/convert.go:
--------------------------------------------------------------------------------
1 | package openapi2
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 |
7 | "github.com/getkin/kin-openapi/openapi2conv"
8 | yaml "gopkg.in/yaml.v3"
9 | )
10 |
11 | func ConvertSpectrumOAS2ToKinOAS2(smSpec *Specification) (*Spec, error) {
12 | bytes, err := json.Marshal(smSpec)
13 | if err != nil {
14 | return nil, err
15 | }
16 | var kinSpec Spec
17 | err = json.Unmarshal(bytes, &kinSpec)
18 | return &kinSpec, err
19 | }
20 |
21 | func ConvertOAS2FileToOAS3File(oas2file, oas3file string, perm os.FileMode, pretty bool) error {
22 | oas2, err := ReadOpenAPI2KinSpecFile(oas2file)
23 | if err != nil {
24 | return err
25 | }
26 | oas3, err := openapi2conv.ToV3(oas2)
27 | if err != nil {
28 | return err
29 | }
30 | if FilenameIsYAML(oas3file) {
31 | bytes, err := yaml.Marshal(oas3)
32 | if err != nil {
33 | return err
34 | }
35 | return os.WriteFile(oas3file, bytes, perm)
36 | }
37 | if bytes, err := oas3.MarshalJSON(); err != nil {
38 | return err
39 | } else {
40 | return os.WriteFile(oas3file, bytes, perm)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/openapi3/path_more.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "reflect"
7 |
8 | oas3 "github.com/getkin/kin-openapi/openapi3"
9 | "github.com/grokify/mogo/net/http/httputilmore"
10 | )
11 |
12 | type PathItemMore struct {
13 | PathItem *oas3.PathItem
14 | }
15 |
16 | func (pm *PathItemMore) AddPathItemOperations(add *oas3.PathItem, overwriteOpration bool) error {
17 | if add == nil {
18 | return nil
19 | } else if pm.PathItem == nil {
20 | return errors.New("path item is not set")
21 | }
22 | methods := httputilmore.Methods()
23 | for _, method := range methods {
24 | opAdd := add.GetOperation(method)
25 | if opAdd == nil {
26 | continue
27 | } else if overwriteOpration {
28 | pm.PathItem.SetOperation(method, opAdd)
29 | } else {
30 | opSrc := pm.PathItem.GetOperation(method)
31 | if opSrc == nil {
32 | pm.PathItem.SetOperation(method, opAdd)
33 | } else if !reflect.DeepEqual(opAdd, opSrc) {
34 | return fmt.Errorf("operation collision on op id (%s)", opSrc.OperationID)
35 | }
36 | }
37 | }
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/openapi3lint/lintutil/scope.go:
--------------------------------------------------------------------------------
1 | package lintutil
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | const (
9 | ScopeOperation = "operation"
10 | ScopeSpecification = "specification"
11 | )
12 |
13 | var mapStringScope = map[string]string{
14 | "operation": ScopeOperation,
15 | "oper": ScopeOperation,
16 | "op": ScopeOperation,
17 | "specification": ScopeSpecification,
18 | "spec": ScopeSpecification,
19 | "": ScopeSpecification,
20 | }
21 |
22 | func ParseScope(s string) (string, error) {
23 | s = strings.ToLower(strings.TrimSpace(s))
24 | if scope, ok := mapStringScope[s]; ok {
25 | return scope, nil
26 | }
27 | return "", fmt.Errorf("unknown scope [%s]", s)
28 | }
29 |
30 | func ScopeMatch(wantScope, tryScope string) bool {
31 | wantScopeCanonical, err := ParseScope(wantScope)
32 | if err != nil {
33 | return false
34 | }
35 | tryScopeCanonical, err := ParseScope(tryScope)
36 | if err != nil {
37 | return false
38 | }
39 | if wantScopeCanonical != tryScopeCanonical {
40 | return false
41 | }
42 | return true
43 | }
44 |
--------------------------------------------------------------------------------
/cmd/oas3lint/policy_api_ringcentral.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RingCentral Policy",
3 | "version": "v0.1.0",
4 | "lastUpdated": "2021-06-10T00:00:00Z",
5 | "includeStandardRules": true,
6 | "rules":{
7 | "datatype-int-format-standard-exist": {
8 | "severity": "error"
9 | },
10 | "operation-operationid-style-camelcase":{
11 | "severity": "error"
12 | },
13 | "operation-summary-exist":{
14 | "severity": "error"
15 | },
16 | "operation-summary-style-first-uppercase":{
17 | "severity": "error"
18 | },
19 | "path-param-style-camelcase":{
20 | "severity": "error"
21 | },
22 | "schema-object-properties-exist":{
23 | "severity": "error"
24 | },
25 | "schema-property-enum-style-pascalcase":{
26 | "severity": "error"
27 | },
28 | "tag-style-first-uppercase":{
29 | "severity": "error"
30 | },
31 | "x-tag-has-group":{
32 | "severity": "error"
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-2025 John Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/cmd/openapi2csv/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "regexp"
7 |
8 | "github.com/grokify/mogo/os/osutil"
9 | csv "github.com/grokify/spectrum/openapi2/openapi2csv"
10 | flags "github.com/jessevdk/go-flags"
11 | )
12 |
13 | type Options struct {
14 | Directory string `short:"d" long:"dir" description:"Source Directory" required:"true"`
15 | Regexp string `short:"r" long:"regexp" description:"matching " required:"true"`
16 | Output string `short:"o" long:"output" description:"Output CSV File" required:"true"`
17 | }
18 |
19 | func main() {
20 | var opts Options
21 | _, err := flags.Parse(&opts)
22 | if err != nil {
23 | log.Fatal(err)
24 | }
25 |
26 | entries, err := osutil.ReadDirMore(
27 | opts.Directory, regexp.MustCompile(opts.Regexp), false, true, false)
28 | if err != nil {
29 | log.Fatal(err)
30 | }
31 | filepaths := entries.Names(opts.Directory)
32 | tbl, err := csv.TableFromSpecFiles(filepaths, true)
33 | if err != nil {
34 | log.Fatalf("TableFromSpecFiles [%v]\n", err.Error())
35 | }
36 |
37 | err = tbl.WriteCSV(opts.Output)
38 | if err != nil {
39 | log.Fatal(err)
40 | }
41 | fmt.Printf("WROTE [%v]\n", opts.Output)
42 |
43 | fmt.Println("DONE")
44 | }
45 |
--------------------------------------------------------------------------------
/openapi3edit/required.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import (
4 | "regexp"
5 |
6 | "github.com/grokify/mogo/type/stringsutil"
7 | "github.com/grokify/spectrum/openapi3"
8 | )
9 |
10 | // SchemaPropertiesSetOptional sets properties as optional if the description matches a regexp
11 | // such as var rxOptionalDefault = regexp.MustCompile(`(?i)\boptional\b`)
12 | func (se *SpecEdit) SchemaPropertiesSetOptional(rxOptional *regexp.Regexp) error {
13 | if se.SpecMore.Spec == nil {
14 | return openapi3.ErrSpecNotSet
15 | }
16 | if rxOptional == nil {
17 | return nil
18 | }
19 | for _, schemaRef := range se.SpecMore.Spec.Components.Schemas {
20 | if len(schemaRef.Ref) == 0 && schemaRef.Value != nil {
21 | required := []string{}
22 | for propName, propRef := range schemaRef.Value.Properties {
23 | if len(propRef.Ref) == 0 && propRef.Value != nil {
24 | if len(propRef.Value.Description) > 0 &&
25 | !rxOptional.MatchString(propRef.Value.Description) {
26 | required = append(required, propName)
27 | }
28 | }
29 | }
30 | if len(required) > 1 {
31 | required = stringsutil.SliceCondenseSpace(required, true, true)
32 | }
33 | schemaRef.Value.Required = required
34 | }
35 | }
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/cmd/openapi2postman/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/grokify/spectrum/openapi2/openapi2postman2"
8 | "github.com/grokify/spectrum/postman2"
9 | flags "github.com/jessevdk/go-flags"
10 | )
11 |
12 | // Convert yaml2json: https://github.com/bronze1man/yaml2json ... yaml2json_darwin_amd64
13 |
14 | type Options struct {
15 | PostmanBase string `short:"b" long:"base" description:"Basic Postman File"`
16 | Postman string `short:"p" long:"postman" description:"Output Postman File" required:"true"`
17 | Swagger string `short:"s" long:"swagger" description:"Input Swagger File" required:"true"`
18 | }
19 |
20 | func main() {
21 | var opts Options
22 | _, err := flags.Parse(&opts)
23 | if err != nil {
24 | log.Fatal(err)
25 | }
26 |
27 | cfg := openapi2postman2.Configuration{
28 | PostmanURLHostname: "{{RINGCENTRAL_SERVER_HOSTNAME}}",
29 | PostmanHeaders: []postman2.Header{{
30 | Key: "Authorization",
31 | Value: "Bearer {{my_access_token}}"}}}
32 |
33 | conv := openapi2postman2.NewConverter(cfg)
34 | err = conv.MergeConvert(opts.Swagger, opts.PostmanBase, opts.Postman)
35 |
36 | if err != nil {
37 | log.Fatal(err)
38 | } else {
39 | fmt.Printf("Wrote %v\n", opts.Postman)
40 | }
41 |
42 | fmt.Println("DONE")
43 | }
44 |
--------------------------------------------------------------------------------
/openapi3/inspect.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // ExtensionNames is not complete yet.
8 | func (sm *SpecMore) ExtensionNames() map[string]int {
9 | extNames := map[string]int{}
10 | for _, schRef := range sm.Spec.Components.Schemas {
11 | if schRef.Value == nil {
12 | continue
13 | }
14 | sch := schRef.Value
15 | for extName := range sch.Extensions {
16 | // for extName := range sch.ExtensionProps.Extensions {
17 | count, ok := extNames[extName]
18 | if !ok {
19 | extNames[extName] = 1
20 | }
21 | extNames[extName] = count + 1
22 | }
23 | }
24 | return extNames
25 | }
26 |
27 | func (sm *SpecMore) HasComponentSchema(componentSchemaName string, caseInsensitiveCaseMatch bool) bool {
28 | componentSchemaName = strings.TrimSpace(componentSchemaName)
29 | if caseInsensitiveCaseMatch {
30 | componentSchemaName = strings.ToLower(componentSchemaName)
31 | }
32 | if len(sm.Spec.Components.Schemas) == 0 {
33 | return false
34 | }
35 | if _, ok := sm.Spec.Components.Schemas[componentSchemaName]; ok {
36 | return true
37 | }
38 | if caseInsensitiveCaseMatch {
39 | for nameTry := range sm.Spec.Components.Schemas {
40 | if strings.EqualFold(strings.TrimSpace(nameTry), componentSchemaName) {
41 | return true
42 | }
43 | }
44 | }
45 | return false
46 | }
47 |
--------------------------------------------------------------------------------
/openapi3lint/rule_collection_simple.go:
--------------------------------------------------------------------------------
1 | package openapi3lint
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "sort"
7 | "strings"
8 | )
9 |
10 | type RuleCollectionSimple struct {
11 | name string
12 | rules map[string]Rule
13 | }
14 |
15 | func (simple RuleCollectionSimple) AddRule(rule Rule) error {
16 | ruleName := rule.Name()
17 | if len(strings.TrimSpace(ruleName)) == 0 {
18 | return errors.New("rule has no name")
19 | }
20 | if simple.rules == nil {
21 | simple.rules = map[string]Rule{}
22 | }
23 | simple.rules[ruleName] = rule
24 | return nil
25 | }
26 |
27 | func (simple RuleCollectionSimple) Name() string {
28 | if len(strings.TrimSpace(simple.name)) > 0 {
29 | return simple.name
30 | }
31 | return strings.Join(simple.RuleNames(), ",")
32 | }
33 |
34 | func (simple RuleCollectionSimple) RuleNames() []string {
35 | names := []string{}
36 | for ruleName := range simple.rules {
37 | names = append(names, ruleName)
38 | }
39 | sort.Strings(names)
40 | return names
41 | }
42 |
43 | func (simple RuleCollectionSimple) RuleExists(ruleName string) bool {
44 | if _, ok := simple.rules[ruleName]; ok {
45 | return true
46 | }
47 | return false
48 | }
49 |
50 | func (simple RuleCollectionSimple) Rule(ruleName string) (Rule, error) {
51 | if rule, ok := simple.rules[ruleName]; ok {
52 | return rule, nil
53 | }
54 | return EmptyRule{}, fmt.Errorf("rule not found [%s]", ruleName)
55 | }
56 |
--------------------------------------------------------------------------------
/cmd/openapi2to3/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 |
8 | "github.com/grokify/mogo/os/osutil"
9 | "github.com/grokify/spectrum/openapi2"
10 | flags "github.com/jessevdk/go-flags"
11 | )
12 |
13 | // install: go get github.com/grokify/spectrum/apps/openapi2to3
14 |
15 | type Options struct {
16 | OAS2File string `short:"i" long:"input" description:"Input filepath" required:"true"`
17 | OAS3File string `short:"o" long:"output" description:"Output filepath" required:"true"`
18 | Pretty []bool `short:"p" long:"pretty" description:"Pretty print output"`
19 | }
20 |
21 | func main() {
22 | opts := Options{}
23 | _, err := flags.Parse(&opts)
24 | if err != nil {
25 | log.Fatal(err)
26 | }
27 | opts.OAS2File = strings.TrimSpace(opts.OAS2File)
28 | opts.OAS3File = strings.TrimSpace(opts.OAS3File)
29 | isFile, err := osutil.IsFile(opts.OAS2File, true)
30 | if err != nil {
31 | log.Fatal(err)
32 | } else if !isFile {
33 | log.Fatalf("E_INPUT_FILE_IS_NOT_NONEMPTY_FILE [%v]", opts.OAS2File)
34 | }
35 |
36 | var wantPretty bool
37 | if len(opts.Pretty) > 0 {
38 | wantPretty = true
39 | } else {
40 | wantPretty = false
41 | }
42 |
43 | err = openapi2.ConvertOAS2FileToOAS3File(opts.OAS2File, opts.OAS3File, 0644, wantPretty)
44 | if err != nil {
45 | log.Fatal(err)
46 | }
47 | fmt.Printf("WROTE [%v]\n", opts.OAS3File)
48 |
49 | fmt.Println("DONE")
50 | }
51 |
--------------------------------------------------------------------------------
/openapi3/spec_table_response_status.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | oas3 "github.com/getkin/kin-openapi/openapi3"
5 | "github.com/grokify/gocharts/v2/data/histogram"
6 | )
7 |
8 | func (sm *SpecMore) StatusCodesHistogram() *histogram.HistogramSets {
9 | hsets := histogram.NewHistogramSets("Response Codes by Endpoint")
10 | if sm.Spec == nil {
11 | return hsets
12 | }
13 | VisitOperations(sm.Spec, func(path, method string, op *oas3.Operation) {
14 | /*
15 | if op == nil ||
16 | op.Responses == nil ||
17 | len(op.Responses) == 0 { // getkin v0.121.0 to v0.122.0
18 | return
19 | }
20 | */
21 | if op == nil || op.Responses == nil {
22 | return
23 | }
24 | responsesMap := op.Responses.Map() // getkin v0.121.0 to v0.122.0
25 | if len(responsesMap) == 0 {
26 | return
27 | }
28 | for responseStatusCode := range responsesMap {
29 | hsets.Add(path, method, responseStatusCode, 1, true)
30 | }
31 | })
32 | return hsets
33 | }
34 |
35 | func (sm *SpecMore) WriteFileXLSXOperationStatusCodes(filename string) error {
36 | hsets := sm.StatusCodesHistogram()
37 | // return hsets.WriteXLSXPivot(filename, hsets.Name, "Method", "Path", "", "")
38 | return hsets.WriteXLSXPivot(filename, histogram.SetsTablePivotOpts{
39 | TableName: hsets.Name,
40 | ColNameHistogramSet: "Method",
41 | ColNameHistogram: "Path",
42 | InclBinCounts: true,
43 | })
44 | }
45 |
--------------------------------------------------------------------------------
/openapi3lint/ruleopsummaryexist/rule_operation_summary_exist.go:
--------------------------------------------------------------------------------
1 | package ruleopsummaryexist
2 |
3 | import (
4 | "strings"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/net/urlutil"
8 | "github.com/grokify/spectrum/openapi3"
9 | "github.com/grokify/spectrum/openapi3lint/lintutil"
10 | )
11 |
12 | type RuleOperationSummaryExist struct {
13 | name string
14 | }
15 |
16 | func NewRule() RuleOperationSummaryExist {
17 | return RuleOperationSummaryExist{
18 | name: lintutil.RulenameOpSummaryExist}
19 | }
20 |
21 | func (rule RuleOperationSummaryExist) Name() string {
22 | return rule.name
23 | }
24 |
25 | func (rule RuleOperationSummaryExist) Scope() string {
26 | return lintutil.ScopeOperation
27 | }
28 |
29 | func (rule RuleOperationSummaryExist) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
30 | vios := []lintutil.PolicyViolation{}
31 | if spec == nil || op == nil {
32 | return vios
33 | }
34 |
35 | summary := strings.TrimSpace(op.Summary)
36 | if len(summary) > 0 {
37 | return vios
38 | }
39 |
40 | return []lintutil.PolicyViolation{{
41 | RuleName: rule.Name(),
42 | Location: urlutil.JoinAbsolute(opPointer, openapi3.PropertySummary),
43 | Value: op.Summary}}
44 | }
45 |
46 | func (rule RuleOperationSummaryExist) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
47 | return []lintutil.PolicyViolation{}
48 | }
49 |
--------------------------------------------------------------------------------
/openapi3lint/extensions/rule_collection.go:
--------------------------------------------------------------------------------
1 | package extensions
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 |
8 | "github.com/grokify/spectrum/openapi3lint"
9 | "github.com/grokify/spectrum/openapi3lint/extensions/ruletaggroupexist"
10 | )
11 |
12 | type RuleCollectionExtensions struct {
13 | name string
14 | ruleNames map[string]int
15 | }
16 |
17 | func NewRuleCollectionExtensions() RuleCollectionExtensions {
18 | rules := RuleCollectionExtensions{
19 | name: "Spectrum OpenAPI 3 Lint Extensions Rule Collection",
20 | ruleNames: map[string]int{}}
21 | names := rules.RuleNames()
22 | for _, name := range names {
23 | rules.ruleNames[name] = 1
24 | }
25 | return rules
26 | }
27 |
28 | func (std RuleCollectionExtensions) Name() string {
29 | return std.name
30 | }
31 |
32 | func (std RuleCollectionExtensions) RuleExists(ruleName string) bool {
33 | if _, ok := std.ruleNames[ruleName]; ok {
34 | return true
35 | }
36 | return false
37 | }
38 |
39 | func (std RuleCollectionExtensions) RuleNames() []string {
40 | rulenames := []string{
41 | ruletaggroupexist.RuleName,
42 | }
43 | sort.Strings(rulenames)
44 | return rulenames
45 | }
46 |
47 | func (std RuleCollectionExtensions) Rule(name string) (openapi3lint.Rule, error) {
48 | name = strings.ToLower(strings.TrimSpace(name))
49 | switch name {
50 | case ruletaggroupexist.RuleName:
51 | return ruletaggroupexist.NewRule(), nil
52 | }
53 |
54 | return openapi3lint.EmptyRule{}, fmt.Errorf("NewExtensionRule: rule [%s] not found", name)
55 | }
56 |
--------------------------------------------------------------------------------
/docs/images/icon_green-hat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/openapi3/extension_props.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | const (
9 | XTagGroups = "x-tag-groups"
10 | XThrottlingGroup = "x-throttling-group"
11 | )
12 |
13 | type ExtensionPropsParent interface{}
14 |
15 | // ExtensionPropStringOrEmpty converts extension prop value from `json.RawMessage` to `string`.
16 | func (om *OperationMore) ExtensionPropStringOrEmpty(key string) string {
17 | if om.Operation == nil {
18 | return ""
19 | }
20 | // str, err := GetExtensionPropString(om.Operation.ExtensionProps, key)
21 | str, err := GetExtensionPropString(om.Operation.Extensions, key)
22 | if err != nil {
23 | return ""
24 | }
25 | return str
26 | }
27 |
28 | // GetExtensionPropStringOrEmpty converts extension prop value from `json.RawMessage` to `string`.
29 | func GetExtensionPropStringOrEmpty(xprops map[string]any, key string) string {
30 | // func GetExtensionPropStringOrEmpty(xprops oas3.ExtensionProps, key string) string {
31 | str, err := GetExtensionPropString(xprops, key)
32 | if err != nil {
33 | return ""
34 | }
35 | return str
36 | }
37 |
38 | // GetExtensionPropString converts extension prop value from `json.RawMessage` to `string`.
39 | func GetExtensionPropString(xprops map[string]any, key string) (string, error) {
40 | // func GetExtensionPropString(xprops oas3.ExtensionProps, key string) (string, error) {
41 | // iface, ok := xprops.Extensions[key]
42 | iface, ok := xprops[key]
43 | if !ok {
44 | return "", fmt.Errorf("extension prop key [%s] not found", key)
45 | }
46 | // Important to use %s instead of %v.
47 | return strings.Trim(fmt.Sprintf("%s", iface), "\""), nil
48 | }
49 |
--------------------------------------------------------------------------------
/examples/ringcentral-engage-digital/convert.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/grokify/spectrum/openapi3/openapi3postman2"
8 | "github.com/grokify/spectrum/postman2"
9 | "github.com/jessevdk/go-flags"
10 | )
11 |
12 | // Convert yaml2json: https://github.com/bronze1man/yaml2json ... yaml2json_darwin_amd64
13 | // var ApiUrlFormat string = "https://{account}.api.engagement.dimelo.com/1.0"
14 |
15 | type Options struct {
16 | PostmanBase string `short:"b" long:"base" description:"Basic Postman File"`
17 | Postman string `short:"p" long:"postman" description:"Output Postman File" required:"true"`
18 | OpenAPISpec string `short:"s" long:"openapispec" description:"Input OpenAPI Spec File" required:"true"`
19 | }
20 |
21 | func main() {
22 | var opts Options
23 | _, err := flags.Parse(&opts)
24 | if err != nil {
25 | log.Fatal(err)
26 | }
27 |
28 | cfg := openapi3postman2.Configuration{
29 | UseXTagGroups: true,
30 | PostmanServerURLBasePath: "1.0",
31 | PostmanServerURL: "{{RINGCENTRAL_ENGAGE_SERVER_URL}}",
32 | PostmanHeaders: []postman2.Header{{
33 | Key: "Authorization",
34 | Value: "Bearer {{RINGCENTRAL_ENGAGE_ACCESS_TOKEN}}"}}}
35 |
36 | conv := openapi3postman2.NewConverter(cfg)
37 |
38 | merge := true
39 |
40 | if merge && len(opts.PostmanBase) > 0 {
41 | err = conv.MergeConvert(opts.OpenAPISpec, opts.PostmanBase, opts.Postman)
42 | } else {
43 | err = conv.ConvertFile(opts.OpenAPISpec, opts.Postman)
44 | }
45 |
46 | if err != nil {
47 | log.Fatal(err)
48 | } else {
49 | fmt.Printf("Wrote %v\n", opts.Postman)
50 | }
51 |
52 | fmt.Println("DONE")
53 | }
54 |
--------------------------------------------------------------------------------
/openapi3/json_pointer_test.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "golang.org/x/exp/slices"
8 | )
9 |
10 | var jsonPointerTests = []struct {
11 | jsonPointer string
12 | document string
13 | path []string
14 | isTopParam bool
15 | isTopSchema bool
16 | }{
17 | {"mydoc.yaml#/components/schemas/FooBar", "mydoc.yaml", []string{"components", "schemas", "FooBar"}, false, true},
18 | {"#/components/schemas/FooBar", "", []string{"components", "schemas", "FooBar"}, false, true},
19 | }
20 |
21 | // TestParseJSONPointer ensures the `ParseJSONPointer` is working properly.
22 | func TestParseJSONPointer(t *testing.T) {
23 | for _, tt := range jsonPointerTests {
24 | ptr, err := ParseJSONPointer(tt.jsonPointer)
25 | if err != nil {
26 | t.Errorf("openapi3.ParseJSONPointer(\"%s\") Error [%s]",
27 | tt.jsonPointer, err.Error())
28 | }
29 | if ptr.Document != tt.document {
30 | t.Errorf("JSONPointer.Document Mismatch: want [%v], got [%v]",
31 | tt.document, ptr.Document)
32 | }
33 | if !slices.Equal(ptr.Path, tt.path) {
34 | t.Errorf("JSONPointer.Path Mismatch: want [%v], got [%v]",
35 | strings.Join(tt.path, ", "), strings.Join(ptr.Path, ", "))
36 | }
37 | _, gotIsTopParam := ptr.IsTopParameter()
38 | if gotIsTopParam != tt.isTopParam {
39 | t.Errorf("JSONPointer.IsTopParameter() Mismatch: want [%v], got [%v]",
40 | tt.isTopParam, gotIsTopParam)
41 | }
42 | _, gotIsTopSchema := ptr.IsTopSchema()
43 | if gotIsTopSchema != tt.isTopSchema {
44 | t.Errorf("JSONPointer.IsTopSchema() Mismatch: want [%v], got [%v]",
45 | tt.isTopSchema, gotIsTopSchema)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/openapi3lint/ruleintstdformat/rule_datatype_int_format_standard_exist.go:
--------------------------------------------------------------------------------
1 | package ruleintstdformat
2 |
3 | import (
4 | oas3 "github.com/getkin/kin-openapi/openapi3"
5 | "github.com/grokify/mogo/net/urlutil"
6 | "github.com/grokify/spectrum/openapi3"
7 | "github.com/grokify/spectrum/openapi3lint/lintutil"
8 | )
9 |
10 | type RuleDatatypeIntFormatStandardExist struct {
11 | name string
12 | }
13 |
14 | func NewRule() RuleDatatypeIntFormatStandardExist {
15 | return RuleDatatypeIntFormatStandardExist{
16 | name: lintutil.RulenameDatatypeIntFormatStandardExist}
17 | }
18 |
19 | func (rule RuleDatatypeIntFormatStandardExist) Name() string {
20 | return rule.name
21 | }
22 |
23 | func (rule RuleDatatypeIntFormatStandardExist) Scope() string {
24 | return lintutil.ScopeSpecification
25 | }
26 |
27 | func (rule RuleDatatypeIntFormatStandardExist) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
28 | return []lintutil.PolicyViolation{}
29 | }
30 |
31 | func (rule RuleDatatypeIntFormatStandardExist) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
32 | vios := []lintutil.PolicyViolation{}
33 | openapi3.VisitTypesFormats(
34 | spec,
35 | func(jsonPointerRoot, oasType, oasFormat string) {
36 | if oasType == openapi3.TypeInteger &&
37 | oasFormat != openapi3.FormatInt32 &&
38 | oasFormat != openapi3.FormatInt64 {
39 | vios = append(vios, lintutil.PolicyViolation{
40 | RuleName: rule.Name(),
41 | Location: urlutil.JoinAbsolute(pointerBase+jsonPointerRoot, "format"),
42 | Value: oasFormat})
43 | }
44 | },
45 | )
46 | return vios
47 | }
48 |
--------------------------------------------------------------------------------
/openapi3/openapi3postman2/configuration.go:
--------------------------------------------------------------------------------
1 | package openapi3postman2
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "strings"
8 |
9 | "github.com/grokify/spectrum/postman2"
10 | )
11 |
12 | //const DefaultContentTypePreferences string = `multipart/form-data,application/json,application/x-www-form-urlencoded,application/xml,text/plain`
13 | //var defaultContentTypePreferencesSlice = strings.Split(DefaultContentTypePreferences, ",")
14 |
15 | // Configuration is a Spectrum configuration that holds information on how
16 | // to create the Postman 2.0 collection including overriding Swagger 2.0
17 | // spec values.
18 | type Configuration struct {
19 | // PostmanServerURLBasePath supports setting the base path as an environment variable
20 | // such as {{MY_API_BASE_URL}}
21 | PostmanServerURLBasePath string `json:"postmanServerUrlApiBasePath,omitempty"`
22 | PostmanServerURL string `json:"postmanServerUrl,omitempty"`
23 | PostmanURLHostname string `json:"postmanURLHostname,omitempty"`
24 | PostmanHeaders []postman2.Header `json:"postmanHeaders,omitempty"`
25 | UseXTagGroups bool `json:"useXTagGroups,omitempty"`
26 | RequestBodyFunc func(urlPath string) string
27 | }
28 |
29 | func ConfigurationReadFile(filename string) (Configuration, error) {
30 | filename = strings.TrimSpace(filename)
31 | cfg := Configuration{}
32 | if len(filename) == 0 {
33 | return cfg, fmt.Errorf("empty file provided [%s]", filename)
34 | }
35 | bytes, err := os.ReadFile(filename)
36 | if err != nil {
37 | return cfg, err
38 | }
39 | err = json.Unmarshal(bytes, &cfg)
40 | return cfg, err
41 | }
42 |
--------------------------------------------------------------------------------
/openapi2/openapi2openapi3/convert.go:
--------------------------------------------------------------------------------
1 | // openapi2openapi3 relies on `swagger2openapi` to convert
2 | // Swagger 2.0 specs to OpenAPI 3.0 specs.
3 | package openapi2openapi3
4 |
5 | import (
6 | "errors"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/grokify/mogo/errors/errorsutil"
11 | "github.com/grokify/mogo/os/executil"
12 | "github.com/grokify/mogo/type/stringsutil"
13 | )
14 |
15 | const cmdSwagger2OpenAPI = "swagger2openapi"
16 |
17 | func Convert(filenames []string, outdir string, renameFunc func(string) string) errorsutil.ErrorInfos {
18 | errinfos := errorsutil.ErrorInfos{}
19 | for _, srcpath := range filenames {
20 | _, srcfile := filepath.Split(srcpath)
21 | outfile := renameFunc(srcfile)
22 | outpath := filepath.Join(outdir, outfile)
23 |
24 | qtr := stringsutil.Quoter{Beg: "", End: ""}
25 | cmd := strings.Join([]string{
26 | cmdSwagger2OpenAPI,
27 | qtr.Quote(srcpath)}, " ")
28 |
29 | _, stderr, err := executil.ExecToFiles(cmd, outpath, "", 0644)
30 | if err != nil {
31 | if err.Error() == "exit status 1" {
32 | ei := errorsutil.ErrorInfo{
33 | Input: srcpath,
34 | Correct: outpath,
35 | Description: cmd,
36 | Error: errors.New(stderr.String())}
37 | errinfos = append(errinfos, &ei)
38 | } else {
39 | ei := errorsutil.ErrorInfo{
40 | Input: srcpath,
41 | Correct: outpath,
42 | Description: cmd,
43 | Error: err}
44 | errinfos = append(errinfos, &ei)
45 | }
46 | } else {
47 | ei := errorsutil.ErrorInfo{
48 | Input: srcpath,
49 | Correct: outpath,
50 | Description: cmd,
51 | Error: nil}
52 | errinfos = append(errinfos, &ei)
53 | }
54 | }
55 | errinfos.Inflate()
56 | return errinfos
57 | }
58 |
--------------------------------------------------------------------------------
/openapi3edit/deprecated.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import (
4 | "regexp"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/spectrum/openapi3"
8 | )
9 |
10 | func (se *SpecEdit) SchemasSetDeprecated(newDeprecated bool) {
11 | if se.SpecMore.Spec == nil {
12 | return
13 | }
14 | for _, schemaRef := range se.SpecMore.Spec.Components.Schemas {
15 | if len(schemaRef.Ref) == 0 && schemaRef.Value != nil {
16 | schemaRef.Value.Deprecated = newDeprecated
17 | }
18 | }
19 | }
20 |
21 | func (se *SpecEdit) OperationsSetDeprecated(newDeprecated bool) {
22 | if se.SpecMore.Spec == nil {
23 | return
24 | }
25 | openapi3.VisitOperations(
26 | se.SpecMore.Spec,
27 | func(path, method string, op *oas3.Operation) {
28 | if op != nil {
29 | op.Deprecated = newDeprecated
30 | }
31 | },
32 | )
33 | }
34 |
35 | var rxDeprecated = regexp.MustCompile(`(?i)\bdeprecated\b`)
36 |
37 | func (se *SpecEdit) SetDeprecatedImplicit() {
38 | if se.SpecMore.Spec == nil {
39 | return
40 | }
41 | openapi3.VisitOperations(
42 | se.SpecMore.Spec,
43 | func(path, method string, op *oas3.Operation) {
44 | if op != nil && rxDeprecated.MatchString(op.Description) {
45 | op.Deprecated = true
46 | }
47 | },
48 | )
49 | for _, schemaRef := range se.SpecMore.Spec.Components.Schemas {
50 | if len(schemaRef.Ref) == 0 && schemaRef.Value != nil {
51 | if rxDeprecated.MatchString(schemaRef.Value.Description) {
52 | schemaRef.Value.Deprecated = true
53 | }
54 | for _, propRef := range schemaRef.Value.Properties {
55 | if len(propRef.Ref) == 0 && propRef.Value != nil {
56 | if rxDeprecated.MatchString(propRef.Value.Description) {
57 | propRef.Value.Deprecated = true
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/openapi2/read.go:
--------------------------------------------------------------------------------
1 | package openapi2
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "regexp"
7 | "strings"
8 |
9 | "sigs.k8s.io/yaml"
10 | )
11 |
12 | func ReadOpenAPI2SpecFile(filename string) (*Specification, error) {
13 | spec, err := ReadOpenAPI2SpecFileDirect(filename)
14 | return &spec, err
15 | }
16 |
17 | func ReadSwagger2SpecFile(filepath string) (Specification, error) {
18 | return ReadOpenAPI2SpecFileDirect(filepath)
19 | }
20 |
21 | func ReadOpenAPI2SpecFileDirect(filename string) (Specification, error) {
22 | var spec Specification
23 | bytes, err := os.ReadFile(filename)
24 | if err != nil {
25 | return spec, err
26 | }
27 | rx := regexp.MustCompile(`.ya?ml$`)
28 | if rx.MatchString(strings.ToLower(strings.TrimSpace(filename))) {
29 | err = yaml.Unmarshal(bytes, &spec)
30 | } else {
31 | err = json.Unmarshal(bytes, &spec)
32 | }
33 | return spec, err
34 | }
35 |
36 | /*func ReadSwagger2Spec(filepath string) (Specification, error) {
37 | bytes, err := os.ReadFile(filepath)
38 | if err != nil {
39 | return Specification{}, err
40 | }
41 | return NewSpecificationFromBytes(bytes)
42 | }*/
43 |
44 | func ReadOpenAPI2KinSpecFile(filename string) (*Spec, error) {
45 | var swag Spec
46 | bytes, err := os.ReadFile(filename)
47 | if err != nil {
48 | return &swag, err
49 | }
50 | if FilenameIsYAML(filename) {
51 | err = yaml.Unmarshal(bytes, &swag)
52 | } else {
53 | err = json.Unmarshal(bytes, &swag)
54 | }
55 | return &swag, err
56 | }
57 |
58 | var rxYAMLExtension = regexp.MustCompile(`.ya?ml$`)
59 |
60 | // FilenameIsYAML checks to see if a filename ends
61 | // in `.yml` or `.yaml` with a case-insensitive match.
62 | func FilenameIsYAML(filename string) bool {
63 | return rxYAMLExtension.MatchString(strings.ToLower(strings.TrimSpace(filename)))
64 | }
65 |
--------------------------------------------------------------------------------
/postman2/headers_util.go:
--------------------------------------------------------------------------------
1 | package postman2
2 |
3 | import (
4 | "strings"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/net/http/httputilmore"
8 | "github.com/grokify/mogo/type/stringsutil"
9 | "github.com/grokify/spectrum/openapi3"
10 | )
11 |
12 | const DefaultMediaTypePreferences string = `multipart/form-data,application/json,application/x-www-form-urlencoded,application/xml,text/plain`
13 |
14 | func DefaultMediaTypePreferencesSlice() []string {
15 | return strings.Split(DefaultMediaTypePreferences, ",")
16 | }
17 |
18 | func AppendPostmanHeaderValueLower(headers []Header, headerName string, options, preferenceOrder []string) ([]Header, string) {
19 | headerName = strings.TrimSpace(headerName)
20 | headerValue := stringsutil.SliceChooseOnePreferredLowerTrimSpace(options, preferenceOrder)
21 | if len(headerName) > 0 && len(headerValue) > 0 {
22 | headers = append(headers, Header{
23 | Key: headerName,
24 | Value: headerValue})
25 | }
26 | return headers, headerValue
27 | }
28 |
29 | func AddOperationReqResMediaTypeHeaders(headers []Header, operation *oas3.Operation, spec *openapi3.Spec, reqPreferences []string, resPreferences []string) ([]Header, string, string, error) {
30 | om := openapi3.OperationMore{Operation: operation}
31 | reqMediaTypes, err := om.RequestMediaTypes(spec)
32 | if err != nil {
33 | return []Header{}, "", "", err
34 | }
35 | headers, reqMediaType := AppendPostmanHeaderValueLower(
36 | headers,
37 | httputilmore.HeaderContentType,
38 | reqMediaTypes,
39 | reqPreferences,
40 | )
41 | headers, resMediaType := AppendPostmanHeaderValueLower(
42 | headers,
43 | httputilmore.HeaderAccept,
44 | om.ResponseMediaTypes(),
45 | resPreferences,
46 | )
47 | return headers, reqMediaType, resMediaType, nil
48 | }
49 |
--------------------------------------------------------------------------------
/openapi3lint/ruleopsummarystylefirstuppercase/rule_operation_summary_first_uppercase.go:
--------------------------------------------------------------------------------
1 | package ruleopsummarystylefirstuppercase
2 |
3 | import (
4 | "strings"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/net/urlutil"
8 | "github.com/grokify/mogo/type/stringsutil"
9 | "github.com/grokify/spectrum/openapi3"
10 | "github.com/grokify/spectrum/openapi3lint/lintutil"
11 | )
12 |
13 | type RuleOperationSummaryStyleFirstUpperCase struct {
14 | name string
15 | }
16 |
17 | func NewRule() RuleOperationSummaryStyleFirstUpperCase {
18 | return RuleOperationSummaryStyleFirstUpperCase{
19 | name: lintutil.RulenameOpSummaryStyleFirstUpperCase}
20 | }
21 |
22 | func (rule RuleOperationSummaryStyleFirstUpperCase) Name() string {
23 | return rule.name
24 | }
25 |
26 | func (rule RuleOperationSummaryStyleFirstUpperCase) Scope() string {
27 | return lintutil.ScopeOperation
28 | }
29 |
30 | func (rule RuleOperationSummaryStyleFirstUpperCase) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
31 | vios := []lintutil.PolicyViolation{}
32 | if spec == nil || op == nil || len(op.Summary) == 0 {
33 | return vios
34 | }
35 |
36 | summary := strings.TrimSpace(op.Summary)
37 | if len(summary) > 0 {
38 | return vios
39 | }
40 | if len(summary) == 0 {
41 | return vios
42 | }
43 |
44 | if summary != stringsutil.ToUpperFirst(summary, false) {
45 | return []lintutil.PolicyViolation{{
46 | RuleName: rule.Name(),
47 | Location: urlutil.JoinAbsolute(opPointer, openapi3.PropertySummary),
48 | Value: op.Summary}}
49 | }
50 | return vios
51 | }
52 |
53 | func (rule RuleOperationSummaryStyleFirstUpperCase) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
54 | return []lintutil.PolicyViolation{}
55 | }
56 |
--------------------------------------------------------------------------------
/examples/ringcentral/convert.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/grokify/spectrum/openapi2/openapi2postman2"
8 | "github.com/grokify/spectrum/openapi3"
9 | "github.com/grokify/spectrum/postman2"
10 | "github.com/jessevdk/go-flags"
11 | )
12 |
13 | // Convert yaml2json: https://github.com/bronze1man/yaml2json ... yaml2json_darwin_amd64
14 |
15 | type Options struct {
16 | PostmanBase string `short:"b" long:"base" description:"Basic Postman File"`
17 | Postman string `short:"p" long:"postman" description:"Output Postman File" required:"true"`
18 | OpenAPISpec string `short:"s" long:"openapispec" description:"Input OpenAPI Spec File" required:"true"`
19 | }
20 |
21 | func main() {
22 | var opts Options
23 | _, err := flags.Parse(&opts)
24 | if err != nil {
25 | log.Fatal(err)
26 | }
27 |
28 | cfg := openapi2postman2.Configuration{
29 | PostmanURLBase: "{{RINGCENTRAL_SERVER_URL}}",
30 | PostmanHeaders: []postman2.Header{{
31 | Key: "Authorization",
32 | Value: "Bearer {{my_access_token}}"}}}
33 |
34 | conv := openapi2postman2.NewConverter(cfg)
35 | err = conv.MergeConvert(opts.OpenAPISpec, opts.PostmanBase, opts.Postman)
36 |
37 | if err != nil {
38 | log.Fatal(err)
39 | } else {
40 | fmt.Printf("Wrote %v\n", opts.Postman)
41 | }
42 |
43 | fmt.Println("DONE")
44 | }
45 |
46 | func ReadFile(filename string) {
47 | spec, err := openapi3.ReadFile(filename, true)
48 | if err != nil {
49 | log.Fatal(err)
50 | }
51 | sm := openapi3.SpecMore{Spec: spec}
52 |
53 | err = sm.SchemaPropertiesWithoutDescriptionsWriteFile("rc-platform.yml.schema-properties_missing-descriptions.txt")
54 | if err != nil {
55 | log.Fatal(err)
56 | }
57 |
58 | err = sm.OperationParametersWithoutDescriptionsWriteFile("rc-platform.yml.op-params_missing-descriptions.txt")
59 | if err != nil {
60 | log.Fatal(err)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/openapi3lint/extensions/ruletaggroupexist/rule_tag_group_exist.go:
--------------------------------------------------------------------------------
1 | package ruletaggroupexist
2 |
3 | import (
4 | "strconv"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/net/urlutil"
8 | "github.com/grokify/spectrum/ext/taggroups"
9 | "github.com/grokify/spectrum/openapi3"
10 | "github.com/grokify/spectrum/openapi3lint/lintutil"
11 | )
12 |
13 | const (
14 | RuleName = "x-tag-has-group"
15 | )
16 |
17 | type RuleTagHasGroup struct {
18 | name string
19 | }
20 |
21 | func NewRule() RuleTagHasGroup {
22 | return RuleTagHasGroup{
23 | name: RuleName}
24 | }
25 |
26 | func (rule RuleTagHasGroup) Name() string {
27 | return RuleName
28 | }
29 |
30 | func (rule RuleTagHasGroup) Scope() string {
31 | return lintutil.ScopeOperation
32 | }
33 |
34 | func (rule RuleTagHasGroup) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
35 | if spec == nil || op == nil || len(op.Tags) == 0 {
36 | return nil
37 | }
38 | // sm := openapi3.SpecMore{Spec: spec}
39 | // tagGroups, err := sm.TagGroups()
40 | tagGroups, err := taggroups.SpecTagGroups(spec)
41 | if err != nil {
42 | vio := lintutil.PolicyViolation{
43 | RuleName: rule.Name(),
44 | Location: "#/x-tag-groups",
45 | Value: err.Error()}
46 | return []lintutil.PolicyViolation{vio}
47 | }
48 |
49 | vios := []lintutil.PolicyViolation{}
50 | for i, tagName := range op.Tags {
51 | if !tagGroups.Exists(tagName) {
52 | vios = append(vios, lintutil.PolicyViolation{
53 | RuleName: rule.Name(),
54 | Location: urlutil.JoinAbsolute(opPointer, openapi3.PropertyTags, strconv.Itoa(i)),
55 | Value: tagName})
56 | }
57 | }
58 | return vios
59 | }
60 |
61 | func (rule RuleTagHasGroup) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
62 | return []lintutil.PolicyViolation{}
63 | }
64 |
--------------------------------------------------------------------------------
/openapi3lint/lintutil/rules.go:
--------------------------------------------------------------------------------
1 | package lintutil
2 |
3 | const (
4 | RulenameDatatypeIntFormatStandardExist = "datatype-int-format-standard-exist"
5 | RuleOpDescExist = "operation-description-exist"
6 | RuleOpIDExist = "operation-operationid-exist"
7 |
8 | RulenameOpIDStyleCamelCase = "operation-operationid-style-camelcase"
9 | RulenameOpIDStyleKebabCase = "operation-operationid-style-kebabcase"
10 | RulenameOpIDStylePascalCase = "operation-operationid-style-pascalcase"
11 | RulenameOpIDStyleSnakeCase = "operation-operationid-style-snakecase"
12 |
13 | RulenameSchemaNameStylePascalCase = "schema-name-style-pascalcase"
14 | RulenameSchemaHasReference = "schema-has-reference"
15 | RulenameSchemaReferenceHasSchema = "schema-reference-has-schema"
16 |
17 | RulenameOpSummaryExist = "operation-summary-exist"
18 | RulenameOpSummaryStyleFirstUpperCase = "operation-summary-style-first-uppercase"
19 |
20 | RuleOpTagsCountOneOnly = "operation-tags-count-one"
21 | RulePathParamNameExist = "path-param-name-exist"
22 |
23 | RulenamePathParamStyleCamelCase = "path-param-style-camelcase"
24 | RulenamePathParamStyleKebabCase = "path-param-style-kebabcase"
25 | RulenamePathParamStylePascalCase = "path-param-style-pascalcase"
26 | RulenamePathParamStyleSnakeCase = "path-param-style-snakecase"
27 |
28 | RulenameSchemaPropEnumStyleCamelCase = "schema-property-enum-style-camelcase"
29 | RulenameSchemaPropEnumStyleKebabCase = "schema-property-enum-style-kebabcase"
30 | RulenameSchemaPropEnumStylePascalCase = "schema-property-enum-style-pascalcase"
31 | RulenameSchemaPropEnumStyleSnakeCase = "schema-property-enum-style-snakecase"
32 |
33 | RuleSchemaPropDescExist = "property-description-exist"
34 |
35 | RulenameSchemaObjectPropsExist = "schema-object-properties-exist"
36 |
37 | RulenameTagStyleFirstUpperCase = "tag-style-first-uppercase"
38 | )
39 |
--------------------------------------------------------------------------------
/docs/openapi3/inspect/missing_descriptions.md:
--------------------------------------------------------------------------------
1 | # Identify Missing Descriptions
2 |
3 | Descriptions are important to aid understanding of various objects in the OpenAPI spec.
4 |
5 | Spectrum provides an ability to list operation parameters and schema properties with missing descriptions.
6 |
7 | ## Missing Operation Parameter Descriptions
8 |
9 | ```golang
10 | specmore := openapi3.SpecMore{Spec: spec}
11 |
12 | // OperationPropertiesDescriptionStatus returns a
13 | // map[string]map[string]int as a `mogo/maputil.MapStringMapStringInt`
14 | // where `1` indicates with desc and `0` indicates without desc.
15 | status := specmore.OperationPropertiesDescriptionStatus()
16 |
17 | // OperationParametersDescriptionStatusCounts returns
18 | // counts of operations with, without and all operations.
19 | countWith, countWithout, countAll :=
20 | specmore.OperationParametersDescriptionStatusCounts()
21 |
22 | // OperationParametersWithoutDescriptionsWriteFile
23 | // will write the operationIds and param names to a file
24 | err := specmore.OperationParametersWithoutDescriptionsWriteFile(
25 | "missing-descs_op-params.txt")
26 | ```
27 |
28 | ## Missing Schema Property Descriptions
29 |
30 | ```golang
31 | specmore := openapi3.SpecMore{Spec: spec}
32 |
33 | // SchemaPropertiesDescriptionStatus returns a
34 | // map[string]map[string]int as a `mogo/maputil.MapStringMapStringInt`
35 | // where `1` indicates with desc and `0` indicates without desc.
36 | status := specmore.SchemaPropertiesDescriptionStatus()
37 |
38 | // SchemaPropertiesDescriptionStatusCounts returns counts of
39 | // schema properties with, without and all operations.
40 | countWith, countWithout, countAll :=
41 | specmore.SchemaPropertiesDescriptionStatusCounts()
42 |
43 | // SchemaPropertiesWithoutDescriptionsWriteFile
44 | // will write the schema names and property names to a file
45 | err := specmore.SchemaPropertiesWithoutDescriptionsWriteFile(
46 | "missing-descs_schema-props.txt")
47 | ```
--------------------------------------------------------------------------------
/openapi3/merge_options.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "reflect"
5 | "regexp"
6 |
7 | oas3 "github.com/getkin/kin-openapi/openapi3"
8 | "github.com/grokify/gocharts/v2/data/table/tabulator"
9 | )
10 |
11 | type CollisionCheckResult int
12 |
13 | const (
14 | CollisionCheckSame CollisionCheckResult = iota
15 | CollisionCheckOverwrite
16 | CollisionCheckError
17 | CollisionCheckSkip
18 | )
19 |
20 | type MergeOptions struct {
21 | FileRx *regexp.Regexp
22 | SchemaFunc func(schemaName string, sch1, sch2 interface{}, hint2 string) CollisionCheckResult
23 | CollisionCheckResult CollisionCheckResult
24 | ValidateEach bool
25 | ValidateFinal bool
26 | TableColumns *tabulator.ColumnSet
27 | TableOpFilterFunc func(path, method string, op *oas3.Operation) bool
28 | TableAddlColFormatFuncs *OperationMoreStringFuncMap
29 | }
30 |
31 | func NewMergeOptionsSkip() *MergeOptions {
32 | return &MergeOptions{
33 | CollisionCheckResult: CollisionCheckSkip,
34 | SchemaFunc: SchemaCheckCollisionSkip}
35 | }
36 |
37 | func (mo *MergeOptions) CheckSchemaCollision(schemaName string, sch1, sch2 interface{}, hint2 string) CollisionCheckResult {
38 | if mo.CollisionCheckResult == CollisionCheckSkip {
39 | mo.SchemaFunc = SchemaCheckCollisionSkip
40 | } else if mo.SchemaFunc == nil {
41 | mo.SchemaFunc = SchemaCheckCollisionDefault
42 | }
43 | return mo.SchemaFunc(schemaName, sch1, sch2, hint2)
44 | }
45 |
46 | func SchemaCheckCollisionDefault(schemaName string, item1, item2 interface{}, item2Note string) CollisionCheckResult {
47 | if reflect.DeepEqual(item1, item2) {
48 | return CollisionCheckSame
49 | }
50 | return CollisionCheckError
51 | }
52 |
53 | func SchemaCheckCollisionSkip(schemaName string, item1, item2 interface{}, item2Note string) CollisionCheckResult {
54 | if reflect.DeepEqual(item1, item2) {
55 | return CollisionCheckSame
56 | }
57 | return CollisionCheckSkip
58 | }
59 |
--------------------------------------------------------------------------------
/openapi2/spec_more.go:
--------------------------------------------------------------------------------
1 | package openapi2
2 |
3 | import (
4 | "net/http"
5 | "sort"
6 |
7 | "github.com/grokify/spectrum"
8 | )
9 |
10 | type SpecMore struct {
11 | Spec *Spec
12 | }
13 |
14 | func (sm *SpecMore) Meta() *spectrum.SpecMeta {
15 | meta := spectrum.NewSpecMeta()
16 | if sm.Spec == nil {
17 | return meta
18 | }
19 | meta.Names = sm.Names()
20 | meta.Inflate()
21 | return meta
22 | }
23 |
24 | func (sm *SpecMore) Names() spectrum.SpecMetaNames {
25 | out := spectrum.SpecMetaNames{
26 | Endpoints: sm.Endpoints(),
27 | Models: sm.ModelNames(),
28 | Paths: sm.PathNames(),
29 | }
30 | return out
31 | }
32 |
33 | func (sm *SpecMore) Endpoints() []string {
34 | var out []string
35 | if sm.Spec == nil {
36 | return out
37 | }
38 | for k, pathItem := range sm.Spec.Paths {
39 | if pathItem.Delete != nil {
40 | out = append(out, spectrum.EndpointString(http.MethodDelete, k))
41 | }
42 | if pathItem.Get != nil {
43 | out = append(out, spectrum.EndpointString(http.MethodGet, k))
44 | }
45 | if pathItem.Head != nil {
46 | out = append(out, spectrum.EndpointString(http.MethodHead, k))
47 | }
48 | if pathItem.Options != nil {
49 | out = append(out, spectrum.EndpointString(http.MethodOptions, k))
50 | }
51 | if pathItem.Patch != nil {
52 | out = append(out, spectrum.EndpointString(http.MethodPatch, k))
53 | }
54 | if pathItem.Post != nil {
55 | out = append(out, spectrum.EndpointString(http.MethodPost, k))
56 | }
57 | if pathItem.Put != nil {
58 | out = append(out, spectrum.EndpointString(http.MethodPut, k))
59 | }
60 | }
61 | sort.Strings(out)
62 | return out
63 | }
64 |
65 | func (sm *SpecMore) ModelNames() []string {
66 | var out []string
67 | for k := range sm.Spec.Definitions {
68 | out = append(out, k)
69 | }
70 | sort.Strings(out)
71 | return out
72 | }
73 |
74 | func (sm *SpecMore) PathNames() []string {
75 | var out []string
76 | for k := range sm.Spec.Paths {
77 | out = append(out, k)
78 | }
79 | sort.Strings(out)
80 | return out
81 | }
82 |
--------------------------------------------------------------------------------
/examples/openapi3table/example_simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
45 |
46 |
--------------------------------------------------------------------------------
/examples/openapi3table/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | "github.com/grokify/gocharts/v2/data/table/tabulator"
9 | "github.com/grokify/spectrum/openapi3"
10 | "github.com/grokify/spectrum/openapi3/openapi3html"
11 | "github.com/jessevdk/go-flags"
12 | )
13 |
14 | type Options struct {
15 | OpenAPISpec string `short:"o" long:"openapispec" description:"Input OpenAPI Spec File" required:"true"`
16 | }
17 |
18 | func main() {
19 | var opts Options
20 | _, err := flags.Parse(&opts)
21 | if err != nil {
22 | log.Fatal(err)
23 | }
24 |
25 | spec, err := openapi3.ReadFile(opts.OpenAPISpec, true)
26 | if err != nil {
27 | log.Fatal(err)
28 | }
29 |
30 | pageParams := openapi3html.PageParams{
31 | PageTitle: spec.Info.Title,
32 | PageLink: "https://developers.ringcentral.com",
33 | TableDomID: "apitable",
34 | ColumnSet: ColumnTexts()}
35 | err = pageParams.AddSpec(spec)
36 | if err != nil {
37 | log.Fatal(err)
38 | }
39 |
40 | pageHTML := openapi3html.SpectrumUIPage(pageParams)
41 |
42 | filename := "api-regisry.html"
43 | err = os.WriteFile(filename, []byte(pageHTML), 0600)
44 | if err != nil {
45 | log.Fatal(err)
46 | }
47 | fmt.Printf("WROTE [%s]\n", filename)
48 |
49 | fmt.Println("DONE")
50 | }
51 |
52 | func ColumnTexts() *tabulator.ColumnSet {
53 | columns := []tabulator.Column{
54 | {
55 | Display: "Method",
56 | Slug: "method"},
57 | {
58 | Display: "Path",
59 | Slug: "path"},
60 | {
61 | Display: "OperationID",
62 | Slug: "operationId"},
63 | {
64 | Display: "Summary",
65 | Slug: "summary"},
66 | {
67 | Display: "Tags",
68 | Slug: "tags"},
69 | {
70 | Display: "API Group",
71 | Slug: "x-api-group"},
72 | {
73 | Display: "Throttling",
74 | Slug: "x-throttling-group"},
75 | {
76 | Display: "App Permission",
77 | Slug: "x-app-permission"},
78 | {
79 | Display: "User Permissions",
80 | Slug: "x-user-permission"},
81 | {
82 | Display: "Docs Level",
83 | Slug: "x-docs-level"},
84 | }
85 | return &tabulator.ColumnSet{Columns: columns}
86 | }
87 |
--------------------------------------------------------------------------------
/docs/openapi3lint/custom_rules.md:
--------------------------------------------------------------------------------
1 | # OpenAPI3Lint - Custom Rules
2 |
3 | Ther eare two parttst of using custom rules:
4 |
5 | 1. Custom Rule
6 | 1. Custom Rule Collection
7 |
8 | Simple rules can be used directly with the simple Rule Collection, `RuleCollectionSimple`, however, more complex rules can be built using custom Rule Collections. Complex rules allow multiple rule names to be used with a single rule definition, with the Rule Collection handling the instantiation.
9 |
10 | ## Custom Rules
11 |
12 | Custom rules are created using the `Rule` interface. After implementing aa custom rule, load it into a `Policy` to execute.
13 |
14 | Use `Policy.AddRule(rule Rule, errorOnCollision bool)` to add a rule.
15 |
16 | ### Rule Interface
17 |
18 | A rule has the following interface:
19 |
20 | ```go
21 | type Rule interface {
22 | Name() string
23 | Scope() string
24 | Severity() string
25 | ProcessSpec(spec *oas3.Swagger, pointerBase string) *lintutil.PolicyViolationsSets
26 | ProcessOperation(spec *oas3.Swagger, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation
27 | }
28 | ```
29 |
30 | #### Functions
31 |
32 | * `Name()` should return the name of a rule in kebab case.
33 | * `Scope()` should return the type of object / property operated on. This affects the processing function provided. As of now, `operation` and `specfication` are supported.
34 | * `Severity()` should return a syslog like severity level supported by `github.com/grokify/mogo/log/severity`. This should be updated for the `Policy` used.
35 | * `ProcessSpec(spec *oas3.Swagger, pointerBase string)` is a function to process a rule at the top specfication level. `pointerBase` is used to provide JSON Pointer info before the `#`. This is executed when `Scope()` is set to `specification`.
36 | * `ProcessOperation(spec *oas3.Swagger, op *oas3.Operation, opPointer, path, method string)` is executed when `Scope()` is set to `operation`.
37 |
38 | ## Rule Collection
39 |
40 | ```go
41 | type RuleCollection interface {
42 | Name() string
43 | RuleNames() []string
44 | RuleExists(ruleName string) bool
45 | Rule(ruleName string) (Rule, error)
46 | }
47 | ```
48 |
--------------------------------------------------------------------------------
/openapi3edit/operations_references_fix.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 |
8 | oas3 "github.com/getkin/kin-openapi/openapi3"
9 | "github.com/grokify/spectrum/openapi3"
10 | )
11 |
12 | const (
13 | oas2BasePathDefinitions = "#/definitions/"
14 | oas3BasePathComponentsSchemas = "#/components/schemas/"
15 | )
16 |
17 | var rxOAS2RefDefinition = regexp.MustCompile(`^#/definitions/(.*)`)
18 |
19 | func (se *SpecEdit) OperationsFixResponseReferences() []*openapi3.OperationMeta {
20 | errorOperations := []*openapi3.OperationMeta{}
21 | if se.SpecMore.Spec == nil {
22 | return errorOperations
23 | }
24 | openapi3.VisitOperations(se.SpecMore.Spec, func(path, method string, op *oas3.Operation) {
25 | if op == nil {
26 | return
27 | }
28 | respsMap := op.Responses.Map() // getkin v0.121.0 to v0.122.0
29 | for resCode, resRef := range respsMap {
30 | // for resCode, resRef := range op.Responses { // getkin v0.121.0 to v0.122.0
31 | if strings.Index(resRef.Ref, oas2BasePathDefinitions) == 0 {
32 | resRef.Ref = strings.TrimSpace(resRef.Ref)
33 | m := rxOAS2RefDefinition.FindStringSubmatch(resRef.Ref)
34 | if len(m) > 0 {
35 | resRefOrig := resRef.Ref
36 | resRef.Ref = oas3BasePathComponentsSchemas + m[1]
37 | om := openapi3.OperationToMeta(path, method, op, []string{})
38 | om.MetaNotes = append(om.MetaNotes, fmt.Sprintf("E_BAD_RESPONSE_REF_OAS2_DEF [%s] type[%s]", resCode, resRefOrig))
39 | errorOperations = append(errorOperations, om)
40 | }
41 | }
42 | }
43 | })
44 | return errorOperations
45 | }
46 |
47 | func FixFile(input, output string, prefix, indent string, validateOutput bool) (*openapi3.Spec, []*openapi3.OperationMeta, error) {
48 | spec, err := openapi3.ReadFile(input, false)
49 | if err != nil {
50 | return spec, []*openapi3.OperationMeta{}, err
51 | }
52 | se := NewSpecEdit(spec)
53 | errs := se.OperationsFixResponseReferences()
54 | output = strings.TrimSpace(output)
55 | if len(output) > 0 {
56 | smore := openapi3.SpecMore{Spec: spec}
57 | return spec, errs, smore.WriteFileJSON(output, 0644, prefix, indent)
58 | }
59 | return spec, errs, err
60 | }
61 |
--------------------------------------------------------------------------------
/openapi3lint/ruletagstylefirstuppercase/rule_tag_style_first_uppercase.go:
--------------------------------------------------------------------------------
1 | package ruletagstylefirstuppercase
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/grokify/mogo/encoding/jsonpointer"
7 | "github.com/grokify/mogo/text/stringcase"
8 | "github.com/grokify/spectrum/openapi3"
9 | "github.com/grokify/spectrum/openapi3lint/lintutil"
10 | )
11 |
12 | type RuleTagStyleFirstUpperCase struct {
13 | name string
14 | }
15 |
16 | func NewRule() RuleTagStyleFirstUpperCase {
17 | return RuleTagStyleFirstUpperCase{
18 | name: lintutil.RulenameTagStyleFirstUpperCase}
19 | }
20 |
21 | func (rule RuleTagStyleFirstUpperCase) Name() string {
22 | return rule.name
23 | }
24 |
25 | func (rule RuleTagStyleFirstUpperCase) Scope() string {
26 | return lintutil.ScopeOperation
27 | }
28 |
29 | func (rule RuleTagStyleFirstUpperCase) ProcessOperation(spec *openapi3.Spec, op *openapi3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
30 | return nil
31 | }
32 |
33 | func (rule RuleTagStyleFirstUpperCase) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
34 | vios := []lintutil.PolicyViolation{}
35 | openapi3.VisitOperations(spec, func(path, method string, op *openapi3.Operation) {
36 | if op == nil {
37 | return
38 | }
39 | opLoc := jsonpointer.PointerSubEscapeAll(
40 | "%s#/paths/%s/%s/tags/",
41 | pointerBase,
42 | path,
43 | method)
44 | for i, tag := range op.Tags {
45 | if !stringcase.IsFirstAlphaUpper(tag) {
46 | vios = append(vios, lintutil.PolicyViolation{
47 | RuleName: rule.Name(),
48 | Location: opLoc + strconv.Itoa(i),
49 | Value: tag})
50 | }
51 | }
52 | })
53 | for i, tag := range spec.Tags {
54 | jsLoc := jsonpointer.PointerSubEscapeAll(
55 | "%s#/tags/%d/name", pointerBase, i)
56 | if tag == nil {
57 | vios = append(vios, lintutil.PolicyViolation{
58 | RuleName: rule.Name(),
59 | Location: jsLoc,
60 | Value: ""})
61 | } else if !stringcase.IsFirstAlphaUpper(tag.Name) {
62 | vios = append(vios, lintutil.PolicyViolation{
63 | RuleName: rule.Name(),
64 | Location: jsLoc,
65 | Value: tag.Name})
66 | }
67 | }
68 | return vios
69 | }
70 |
--------------------------------------------------------------------------------
/openapi3lint/ruleschemareferences/rule_schema_references_exist.go:
--------------------------------------------------------------------------------
1 | package ruleschemareferences
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | oas3 "github.com/getkin/kin-openapi/openapi3"
8 | "github.com/grokify/spectrum/openapi3"
9 | "github.com/grokify/spectrum/openapi3lint/lintutil"
10 | )
11 |
12 | type RuleSchemaReferences struct {
13 | name string
14 | }
15 |
16 | func NewRule(ruleName string) (RuleSchemaReferences, error) {
17 | ruleNameCanonical := strings.ToLower(strings.TrimSpace(ruleName))
18 | rule := RuleSchemaReferences{
19 | name: ruleNameCanonical}
20 | if ruleNameCanonical != lintutil.RulenameSchemaHasReference &&
21 | ruleNameCanonical != lintutil.RulenameSchemaReferenceHasSchema {
22 | return rule, fmt.Errorf("rule [%s] not supported", ruleName)
23 | }
24 | return rule, nil
25 | }
26 |
27 | func (rule RuleSchemaReferences) Name() string {
28 | return rule.name
29 | }
30 |
31 | func (rule RuleSchemaReferences) Scope() string {
32 | return lintutil.ScopeSpecification
33 | }
34 |
35 | func (rule RuleSchemaReferences) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
36 | return []lintutil.PolicyViolation{}
37 | }
38 |
39 | func (rule RuleSchemaReferences) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
40 | violations := []lintutil.PolicyViolation{}
41 |
42 | sm := openapi3.SpecMore{Spec: spec}
43 | schemaNoRef, _, refNoSchema, err := sm.SchemaNamesStatus()
44 | if err != nil {
45 | return violations
46 | }
47 | switch rule.name {
48 | case lintutil.RulenameSchemaHasReference:
49 | for _, schemaName := range schemaNoRef {
50 | violations = append(violations, lintutil.PolicyViolation{
51 | RuleName: lintutil.RulenameSchemaHasReference,
52 | Location: openapi3.SchemaPointerExpand(pointerBase, schemaName)})
53 | }
54 | case lintutil.RulenameSchemaReferenceHasSchema:
55 | for _, schemaName := range refNoSchema {
56 | violations = append(violations, lintutil.PolicyViolation{
57 | RuleName: lintutil.RulenameSchemaReferenceHasSchema,
58 | Location: openapi3.SchemaPointerExpand(pointerBase, schemaName)})
59 | }
60 | }
61 | return violations
62 | }
63 |
--------------------------------------------------------------------------------
/cmd/spectrum/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 |
8 | "github.com/grokify/mogo/errors/errorsutil"
9 | "github.com/grokify/spectrum/openapi3"
10 | "github.com/grokify/spectrum/openapi3/openapi3postman2"
11 | flags "github.com/jessevdk/go-flags"
12 | )
13 |
14 | // Convert yaml2json: https://github.com/bronze1man/yaml2json ... yaml2json_darwin_amd64
15 |
16 | type Options struct {
17 | OpenAPIFile string `short:"O" long:"openapiFile" description:"Input Swagger File" required:"true"`
18 | Config string `short:"C" long:"config" description:"Spectrum Config File"`
19 | PostmanBase string `short:"B" long:"basePostmanFile" description:"Basic Postman File"`
20 | Postman string `short:"P" long:"postmanFile" description:"Output Postman File"`
21 | XLSXFile string `short:"X" long:"xlsxFile" description:"Output XLSX File"`
22 | }
23 |
24 | func (opts *Options) TrimSpace() {
25 | opts.Config = strings.TrimSpace(opts.Config)
26 | opts.PostmanBase = strings.TrimSpace(opts.PostmanBase)
27 | opts.Postman = strings.TrimSpace(opts.Postman)
28 | opts.OpenAPIFile = strings.TrimSpace(opts.OpenAPIFile)
29 | }
30 |
31 | func main() {
32 | var opts Options
33 | _, err := flags.Parse(&opts)
34 | if err != nil {
35 | log.Fatal(err)
36 | }
37 |
38 | spec, err := openapi3.ReadFile(opts.OpenAPIFile, true)
39 | if err != nil {
40 | log.Fatal(err)
41 | }
42 |
43 | if len(opts.Postman) > 0 {
44 | cfg3 := openapi3postman2.Configuration{}
45 |
46 | if len(opts.Config) > 0 {
47 | cfg3, err = openapi3postman2.ConfigurationReadFile(opts.Config)
48 | if err != nil {
49 | log.Fatal(errorsutil.Wrap(err, "openapi3postman2.ConfigurationReadFile"))
50 | }
51 | }
52 |
53 | conv := openapi3postman2.Converter{
54 | Configuration: cfg3,
55 | OpenAPISpec: spec}
56 |
57 | err = conv.MergeConvert(
58 | opts.OpenAPIFile,
59 | opts.PostmanBase,
60 | opts.Postman)
61 |
62 | if err != nil {
63 | log.Fatal(errorsutil.Wrap(err, "spectrum.main << conv.MergeConvert"))
64 | }
65 |
66 | fmt.Printf("wrote Postman collection [%s]\n", opts.Postman)
67 | }
68 | if len(opts.XLSXFile) > 0 {
69 | sm := openapi3.SpecMore{Spec: spec}
70 | err := sm.WriteFileXLSX(opts.XLSXFile, nil, nil, nil)
71 | if err != nil {
72 | log.Fatal(err)
73 | }
74 | }
75 | fmt.Println("DONE")
76 | }
77 |
--------------------------------------------------------------------------------
/openapi3/intersection.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "sort"
5 |
6 | "github.com/grokify/mogo/type/stringsutil"
7 | )
8 |
9 | type SpecMetadata struct {
10 | Endpoints []string
11 | OperationIDs []string
12 | SchemaNames []string
13 | }
14 |
15 | func NewSpecMetadata() SpecMetadata {
16 | return SpecMetadata{
17 | Endpoints: []string{},
18 | OperationIDs: []string{},
19 | SchemaNames: []string{}}
20 | }
21 |
22 | func (sm *SpecMore) Metadata() SpecMetadata {
23 | md := NewSpecMetadata()
24 | if sm.Spec == nil {
25 | return md
26 | }
27 | mapOpIDs := sm.OperationIDsCounts()
28 | for key := range mapOpIDs {
29 | md.OperationIDs = append(md.OperationIDs, key)
30 | }
31 | md.Endpoints = sm.PathMethods(true)
32 | md.SchemaNames = sm.SchemaNames()
33 | return md
34 | }
35 |
36 | func (md *SpecMetadata) Intersection(md2 SpecMetadata) SpecMetadata {
37 | idata := SpecMetadata{
38 | Endpoints: stringsutil.SliceIntersection(md.Endpoints, md2.Endpoints),
39 | OperationIDs: stringsutil.SliceIntersection(md.OperationIDs, md2.OperationIDs),
40 | SchemaNames: stringsutil.SliceIntersection(md.SchemaNames, md2.SchemaNames)}
41 | return idata
42 | }
43 |
44 | func (md *SpecMetadata) IsEmpty() bool {
45 | if len(md.Endpoints) == 0 &&
46 | len(md.OperationIDs) == 0 &&
47 | len(md.SchemaNames) == 0 {
48 | return true
49 | }
50 | return false
51 | }
52 |
53 | func (md *SpecMetadata) Sort() {
54 | sort.Strings(md.Endpoints)
55 | sort.Strings(md.OperationIDs)
56 | sort.Strings(md.SchemaNames)
57 | }
58 |
59 | type IntersectionData struct {
60 | Spec1 SpecMetadata
61 | Spec2 SpecMetadata
62 | Intersection SpecMetadata
63 | }
64 |
65 | func (idata *IntersectionData) Sort() {
66 | idata.Spec1.Sort()
67 | idata.Spec2.Sort()
68 | idata.Intersection.Sort()
69 | }
70 |
71 | func NewIntersectionData() IntersectionData {
72 | return IntersectionData{
73 | Spec1: NewSpecMetadata(),
74 | Spec2: NewSpecMetadata(),
75 | Intersection: NewSpecMetadata()}
76 | }
77 |
78 | func SpecsIntersection(spec1, spec2 *Spec) IntersectionData {
79 | sm1 := SpecMore{Spec: spec1}
80 | sm2 := SpecMore{Spec: spec2}
81 | idata := IntersectionData{
82 | Spec1: sm1.Metadata(),
83 | Spec2: sm2.Metadata()}
84 | idata.Intersection = idata.Spec1.Intersection(idata.Spec2)
85 | return idata
86 | }
87 |
--------------------------------------------------------------------------------
/openapi3lint/ruleopidstyle/rule_operation_operationid_style_camelcase.go:
--------------------------------------------------------------------------------
1 | package ruleopidstyle
2 |
3 | import (
4 | "fmt"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/net/urlutil"
8 | "github.com/grokify/mogo/text/stringcase"
9 | "github.com/grokify/spectrum/openapi3"
10 | "github.com/grokify/spectrum/openapi3lint/lintutil"
11 | )
12 |
13 | type RuleOperationOperationIDStyle struct {
14 | name string
15 | stringCase string
16 | }
17 |
18 | func NewRule(requiredStringCase string) (RuleOperationOperationIDStyle, error) {
19 | canonicalCase, err := stringcase.Parse(requiredStringCase)
20 | if err != nil {
21 | return RuleOperationOperationIDStyle{},
22 | fmt.Errorf("invalid string case [%s]", requiredStringCase)
23 | }
24 | rule := RuleOperationOperationIDStyle{
25 | stringCase: canonicalCase}
26 | switch canonicalCase {
27 | case stringcase.CamelCase:
28 | rule.name = lintutil.RulenameOpIDStyleCamelCase
29 | case stringcase.KebabCase:
30 | rule.name = lintutil.RulenameOpIDStyleKebabCase
31 | case stringcase.PascalCase:
32 | rule.name = lintutil.RulenameOpIDStylePascalCase
33 | case stringcase.SnakeCase:
34 | rule.name = lintutil.RulenameOpIDStyleSnakeCase
35 | default:
36 | return rule, fmt.Errorf("invalid string case [%s]", canonicalCase)
37 | }
38 | return rule, nil
39 | }
40 |
41 | func (rule RuleOperationOperationIDStyle) Name() string {
42 | return rule.name
43 | }
44 |
45 | func (rule RuleOperationOperationIDStyle) Scope() string {
46 | return lintutil.ScopeOperation
47 | }
48 |
49 | func (rule RuleOperationOperationIDStyle) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
50 | if spec == nil || op == nil || len(op.OperationID) == 0 {
51 | return nil
52 | }
53 | isWantCase, err := stringcase.IsCase(rule.stringCase, op.OperationID)
54 | if err == nil && isWantCase {
55 | return nil
56 | }
57 | vio := lintutil.PolicyViolation{
58 | RuleName: rule.Name(),
59 | Location: urlutil.JoinAbsolute(opPointer, openapi3.PropertyOperationID),
60 | Value: op.OperationID}
61 | if err != nil {
62 | vio.Data = map[string]string{
63 | "error": err.Error()}
64 | }
65 | return []lintutil.PolicyViolation{vio}
66 | }
67 |
68 | func (rule RuleOperationOperationIDStyle) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
69 | return []lintutil.PolicyViolation{}
70 | }
71 |
--------------------------------------------------------------------------------
/openapi3/openapi3postman2/folders.go:
--------------------------------------------------------------------------------
1 | package openapi3postman2
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | oas3 "github.com/getkin/kin-openapi/openapi3"
8 | "github.com/grokify/mogo/net/http/httputilmore"
9 | "github.com/grokify/spectrum/ext/taggroups"
10 | "github.com/grokify/spectrum/openapi3"
11 | "github.com/grokify/spectrum/postman2"
12 | )
13 |
14 | func CreateTagsAndTagGroups(pman postman2.Collection, spec *openapi3.Spec) (postman2.Collection, error) {
15 | // oas3specMore := openapi3.SpecMore{Spec: spec}
16 | tagGroupSet, err := taggroups.SpecTagGroups(spec)
17 | // tagGroupSet, err := openapi3.SpecTagGroups(spec)
18 | if err != nil {
19 | return pman, err
20 | }
21 | if len(tagGroupSet.TagGroups) > 0 {
22 | return addFoldersFromTagGroups(pman, tagGroupSet, spec.Tags)
23 | }
24 | return addFoldersFromTags(pman, spec.Tags), nil
25 | }
26 |
27 | func addFoldersFromTagGroups(pman postman2.Collection, tgSet taggroups.TagGroupSet, tags oas3.Tags) (postman2.Collection, error) {
28 | tagsMore := openapi3.TagsMore{Tags: tags}
29 | for _, tg := range tgSet.TagGroups {
30 | tg.Name = strings.TrimSpace(tg.Name)
31 | if len(tg.Name) == 0 && len(tg.Tags) > 0 {
32 | return pman, fmt.Errorf("E_TAG_GROUP_EMPTY_NAME TAGS [%s]", strings.Join(tg.Tags, ","))
33 | }
34 | topFolder := pman.GetOrNewFolder(tg.Name)
35 | if topFolder.Item == nil {
36 | topFolder.Item = []*postman2.Item{}
37 | }
38 | for _, tagName := range tg.Tags {
39 | tagName = strings.TrimSpace(tagName)
40 | if len(tagName) == 0 {
41 | continue
42 | }
43 | subFolder := &postman2.Item{Name: tagName}
44 | tag := tagsMore.Get(tagName)
45 | if tag != nil {
46 | subFolder.Description = &postman2.Description{
47 | Content: strings.TrimSpace(tag.Description),
48 | Type: httputilmore.ContentTypeTextPlain}
49 | }
50 | topFolder.UpsertSubItem(subFolder)
51 | }
52 | pman.SetFolder(topFolder)
53 | }
54 | return pman, nil
55 | }
56 |
57 | func addFoldersFromTags(pman postman2.Collection, tags oas3.Tags) postman2.Collection {
58 | for _, tag := range tags {
59 | if tag == nil {
60 | continue
61 | }
62 | folder := &postman2.Item{
63 | Name: strings.TrimSpace(tag.Name),
64 | Description: &postman2.Description{
65 | Content: strings.TrimSpace(tag.Description),
66 | Type: httputilmore.ContentTypeTextPlain}}
67 | if len(folder.Name) == 0 {
68 | continue
69 | }
70 | pman.SetFolder(folder)
71 | }
72 | return pman
73 | }
74 |
--------------------------------------------------------------------------------
/openapi2/openapi2csv/spec_to_csv.go:
--------------------------------------------------------------------------------
1 | package openapi2csv
2 |
3 | import (
4 | "net/http"
5 | "path/filepath"
6 |
7 | oas2 "github.com/getkin/kin-openapi/openapi2"
8 | "github.com/grokify/gocharts/v2/data/table"
9 | "github.com/grokify/spectrum/openapi2"
10 | )
11 |
12 | func TableFromSpecFiles(files []string, includeFilename bool) (*table.Table, error) {
13 | tbl := table.NewTable("")
14 | tblp := &tbl
15 | tbl.Columns = []string{}
16 | if includeFilename {
17 | tbl.Columns = append(tbl.Columns, "Filename")
18 | }
19 | tbl.Columns = append(tbl.Columns, []string{"Path", "Method", "OperationID", "Summary", "Description"}...)
20 | for _, file := range files {
21 | spec, err := openapi2.ReadOpenAPI2KinSpecFile(file)
22 | if err != nil {
23 | return tblp, err
24 | }
25 | prefix := []string{}
26 | if includeFilename {
27 | _, filename := filepath.Split(file)
28 | prefix = []string{filename}
29 | }
30 | tblp = TableAddOpenAPI2Spec(tblp, spec, prefix)
31 | }
32 | return tblp, nil
33 | }
34 |
35 | func TableAddOpenAPI2Spec(tbl *table.Table, spec *openapi2.Spec, prefix []string) *table.Table {
36 | for url, path := range spec.Paths {
37 | tbl = TableAddOpenAPI2Path(tbl, path, append(prefix, url))
38 | }
39 | return tbl
40 | }
41 |
42 | // prefix can be `filename`,`path`
43 | func TableAddOpenAPI2Path(tbl *table.Table, path *oas2.PathItem, prefix []string) *table.Table {
44 | if path.Delete != nil {
45 | tbl.Rows = append(tbl.Rows, pathOpenAPI2ToRow(prefix, path.Delete, http.MethodDelete))
46 | }
47 | if path.Get != nil {
48 | tbl.Rows = append(tbl.Rows, pathOpenAPI2ToRow(prefix, path.Get, http.MethodGet))
49 | }
50 | if path.Head != nil {
51 | tbl.Rows = append(tbl.Rows, pathOpenAPI2ToRow(prefix, path.Head, http.MethodHead))
52 | }
53 | if path.Options != nil {
54 | tbl.Rows = append(tbl.Rows, pathOpenAPI2ToRow(prefix, path.Options, http.MethodOptions))
55 | }
56 | if path.Patch != nil {
57 | tbl.Rows = append(tbl.Rows, pathOpenAPI2ToRow(prefix, path.Patch, http.MethodPatch))
58 | }
59 | if path.Post != nil {
60 | tbl.Rows = append(tbl.Rows, pathOpenAPI2ToRow(prefix, path.Post, http.MethodPost))
61 | }
62 | if path.Put != nil {
63 | tbl.Rows = append(tbl.Rows, pathOpenAPI2ToRow(prefix, path.Put, http.MethodPut))
64 | }
65 | return tbl
66 | }
67 |
68 | func pathOpenAPI2ToRow(prefix []string, op *oas2.Operation, method string) []string {
69 | row := prefix
70 | row = append(row, method, op.OperationID, op.Summary, op.Description)
71 | return row
72 | }
73 |
--------------------------------------------------------------------------------
/openapi3edit/README.md:
--------------------------------------------------------------------------------
1 | # Spectrum OpenAPI 3 Inspect, Modify & Compare
2 |
3 | [![Docs][docs-godoc-svg]][docs-godoc-link]
4 |
5 | Spectrum `openapi3edit` is a library to assist in inspecting and modifying OpenAPI specs.
6 |
7 | OpenAPI specifications can be large and have many endpoints which can make it difficult to manage. Additionally, some services may consist of many specs created by different people, teams and software, so some ability to make various specs consistent is desirable, especially when the individual specs need to be merged into a master spec.
8 |
9 | Key Features include:
10 |
11 | * Inspect: Various functions to examine aspects of a OpenAPI 3 spec including OperationIDs, paths, endpoint, schemas, tags, etc.
12 | * Modify: Ability to modify various properties programmatically.
13 | * Intersection: Ability to compare two specs and show the overlap.
14 |
15 | ## Usage
16 |
17 | Steps for clean merging of multiple specs.
18 |
19 | 1. Examine all specs for consistent operationIds and tags
20 | 1. Ensure that all specs can be merged to common server base URL and paths
21 | 1. Optionally delete endpoint security from each spec and add it to merged spec
22 | 1. Check / deletee overlapping operationIDs, endpoints (method+path) and schema components
23 | 1. Validate resulting spec
24 |
25 | ### Inspect & Modify
26 |
27 | Use `SpecMoreModifyMulti` and `SpecMoreModifyMultiOpts` to handle
28 | to inspect and modify mulitple files.
29 |
30 | ### Compare
31 |
32 | Use `openapi3edit.SpecsIntersection()`
33 |
34 | ```
35 | // spec1 and spec2 are *github.com/getkin/kin-openapi/openapi3.Swagger
36 | intersectionData := openapi3edit.SpecsIntersection(spec1, spec2)
37 | intersectionData.Sort()
38 | ```
39 |
40 | ### Delete
41 |
42 | After running intersection, you can use the resulting data to delete those items from a spec using `SpecDeleteProperties`. Be sure to validate afterwards.
43 |
44 | This is useful when merging to specs with an overlap. To check for cleanliness of merging, you can:
45 |
46 | 1. run an intersection
47 | 1. delete the intersection from one of the sepcs and ensures it still validates
48 | 1. merge the specs
49 |
50 |
51 | ## Examples
52 |
53 | ### Add Bearer Token Auth
54 |
55 | ```go
56 | openapi3edit.SecuritySchemeAddBearertoken(
57 | spec, "", "",
58 | []string{},
59 | []string{
60 | "Authentication",
61 | },
62 | )
63 | ```
64 |
65 | [docs-godoc-svg]: https://pkg.go.dev/badge/github.com/grokify/spectrum
66 | [docs-godoc-link]: https://pkg.go.dev/github.com/grokify/spectrum/openapi3edit
--------------------------------------------------------------------------------
/docs/openapi3_fix.md:
--------------------------------------------------------------------------------
1 | # OpenAPI 3 Specs Auto-Fixer
2 |
3 | In addition to enabling introspection and programmatic modification of
4 | OpenAPI 3 specifications, Spectrum can automatically fix/update some
5 | issues with specs.
6 |
7 | ## Path Parameters - Examination and Resolution
8 |
9 | Path parameters are required to be defined. The following will identify or
10 | automatically add add operation path parameters if they are missing. It
11 | will also move path parameters to the top of the parameter list and maintain
12 | the order in which they appear in the URL path. Other parameters will
13 | maintain their original order.
14 |
15 | ```go
16 | // `spec` is *openapi3.Swagger, `true` indicates whether to auto-fix.
17 | ops, err = modify.ValidateFixOperationPathParameters(spec, true)
18 | if err != nil {
19 | fmtutil.PrintJSON(ops)
20 | log.Fatal(err)
21 | }
22 | ```
23 |
24 | ## Response Type - Examination and Resolution
25 |
26 | Sometimes a spec can be misdefined to use a `application/json` response MIME
27 | type when the schema returned doesn't support JSON, e.g. with the type is
28 | `string` or `integer`. The following will examine and optionally update the
29 | type to `text/plain` to resolve then issue when the response is mis-classified
30 | as `application/json` or some other incompatible MIME type.
31 |
32 | ```go
33 | // `spec` is *openapi3.Swagger, `true` indicates whether to auto-fix.
34 | ops, err := modify.ValidateFixOperationResponseTypes(spec, true)
35 | if err != nil {
36 | fmtutil.PrintJSON(ops)
37 | log.Fatal(err)
38 | }
39 | ```
40 |
41 | ## Move Request Bodies
42 |
43 | Some OpenAPI 3 spec defintions can use request body references like the following
44 | which may not be supported by all tools.
45 |
46 | ```json
47 | {
48 | "requestBody": {
49 | "$ref": "#/components/requestBodies/MyObject"
50 | }
51 | }
52 | ```
53 |
54 | Some tools are better able to handle a `requestBody` definition
55 | as follows:
56 |
57 | ```json
58 | {
59 | "requestBody": {
60 | "content": {
61 | "application/json": {
62 | "schema": {
63 | "$ref": "#/components/schemas/MyObject"
64 | }
65 | }
66 | }
67 | }
68 | }
69 | ```
70 |
71 | The following will move the request body definition so that `content`
72 | and MIME types are specified directly in the operation definition.
73 |
74 | ```go
75 | // `spec` is *openapi3.Swagger, `true` indicates whether to auto-fix.
76 | ops, err = modify.MoveRequestBodies(spec, true)
77 | if err != nil {
78 | fmtutil.PrintJSON(ops)
79 | log.Fatal(err)
80 | }
81 | ```
--------------------------------------------------------------------------------
/openapi3edit/schemas_keys.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import (
4 | "errors"
5 | "regexp"
6 |
7 | "github.com/grokify/mogo/errors/errorsutil"
8 | "github.com/grokify/mogo/type/maputil"
9 | "github.com/grokify/mogo/type/stringsutil/transform"
10 | "github.com/grokify/spectrum/openapi3"
11 | )
12 |
13 | func (se *SpecEdit) SchemaKeysModify(xf func(string) string) error {
14 | if se.SpecMore.Spec == nil {
15 | return openapi3.ErrSpecNotSet
16 | } else if xf == nil {
17 | return nil
18 | }
19 | spec := se.SpecMore.Spec
20 | specSchemaKeysModifySchemaRefs(spec, xf)
21 | err := specSchemaKeysModifySchemaKeys(spec, xf)
22 | if err != nil {
23 | return errorsutil.Wrap(err, "SpecSchemaKeysModifySchemaKeys")
24 | }
25 | return se.SpecMore.Validate()
26 | }
27 |
28 | func specSchemaKeysModifySchemaRefs(spec *openapi3.Spec, xf func(string) string) {
29 | se := NewSpecEdit(spec)
30 | se.SchemaRefsModify(FuncSchemaRefModFromSchemaKeyMod(xf))
31 | }
32 |
33 | var rxComponentSchemasKey = regexp.MustCompile(`^(.*#/components/schemas/)([^/]+)(.*)$`)
34 |
35 | // FuncSchemaRefModFromSchemaKeyMod takles a function for modifying schema keys and turns
36 | // it into a function for modifying JSON schema pointers for schemas keys.
37 | func FuncSchemaRefModFromSchemaKeyMod(xf func(string) string) func(string) string {
38 | return func(s string) string {
39 | m := rxComponentSchemasKey.FindStringSubmatch(s)
40 | if len(m) == 0 {
41 | return s
42 | }
43 | substr := m[2]
44 | mod := xf(substr)
45 | if substr == mod {
46 | return s
47 | }
48 | return m[1] + mod + m[3]
49 | }
50 | }
51 |
52 | // specSchemaKeysModifySchemaKeys only modifies keys in `components.schemas`. Running this
53 | // by itself does not result in a validating spec.
54 | func specSchemaKeysModifySchemaKeys(spec *openapi3.Spec, xf func(string) string) error {
55 | schKeys := maputil.StringKeys(spec.Components.Schemas, nil)
56 | xfMap, _, err := transform.TransformMap(xf, schKeys)
57 | if err != nil {
58 | return err
59 | }
60 | if !maputil.UniqueValues(xfMap) {
61 | return errors.New("collisions")
62 | }
63 | for _, schKey := range schKeys {
64 | schRef, ok := spec.Components.Schemas[schKey]
65 | if !ok {
66 | panic("schema key not found")
67 | }
68 | newSchKey := xf(schKey)
69 | if schKey == newSchKey {
70 | continue
71 | }
72 | spec.Components.Schemas[newSchKey] = schRef
73 | delete(spec.Components.Schemas, schKey)
74 | }
75 | schKeysNew := maputil.StringKeys(spec.Components.Schemas, nil)
76 | if len(schKeys) != len(schKeysNew) {
77 | return errors.New("old and new key mismatch")
78 | }
79 | return nil
80 | }
81 |
--------------------------------------------------------------------------------
/cmd/oas3validate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log/slog"
5 | "os"
6 | "strings"
7 |
8 | "github.com/grokify/gocharts/v2/data/histogram"
9 | "github.com/grokify/mogo/fmt/fmtutil"
10 | "github.com/grokify/mogo/net/urlutil"
11 | "github.com/grokify/spectrum/openapi3"
12 | flags "github.com/jessevdk/go-flags"
13 | )
14 |
15 | type Options struct {
16 | SpecFileOAS3 string `short:"s" long:"specfile" description:"Input OAS Spec File" required:"true"`
17 | WriteOpStatusCodeXlsx string `long:"writeopstatus" description:"Output File" required:"false"`
18 | XlsxWrite string `short:"x" long:"xlsxwrite" description:"Output File" required:"false"`
19 | }
20 |
21 | func main() {
22 | var opts Options
23 | _, err := flags.Parse(&opts)
24 | if err != nil {
25 | slog.Error(err.Error())
26 | os.Exit(1)
27 | }
28 |
29 | var spec *openapi3.Spec
30 |
31 | if urlutil.IsHTTP(opts.SpecFileOAS3, true, true) {
32 | spec, err = openapi3.ReadURL(opts.SpecFileOAS3)
33 | } else {
34 | spec, err = openapi3.ReadFile(opts.SpecFileOAS3, true)
35 | }
36 | if err != nil {
37 | slog.Error(err.Error())
38 | os.Exit(2)
39 | }
40 |
41 | sm := openapi3.SpecMore{Spec: spec}
42 |
43 | slog.Info(
44 | "validSpecInfo",
45 | "filename", opts.SpecFileOAS3,
46 | "title", spec.Info.Title,
47 | "opsCount", sm.OperationsCount())
48 |
49 | sortBy := histogram.SortValueDesc
50 | ops := sm.OperationCountsByTag()
51 | err = ops.WriteTableASCII(os.Stdout,
52 | []string{"Tag", "Operation Count"}, sortBy, true)
53 | if err != nil {
54 | slog.Error(err.Error())
55 | os.Exit(3)
56 | }
57 |
58 | ops2 := ops.ItemCounts(sortBy)
59 | err = fmtutil.PrintJSON(ops2)
60 | if err != nil {
61 | slog.Error(err.Error())
62 | os.Exit(3)
63 | }
64 |
65 | md := ops2.Markdown("1. Count: ", ", Category: ", true, true)
66 | slog.Info(md)
67 | opts.XlsxWrite = strings.TrimSpace(opts.XlsxWrite)
68 | if len(opts.XlsxWrite) > 0 {
69 | err := sm.WriteFileXLSX(opts.XlsxWrite, nil, nil, nil)
70 | if err != nil {
71 | slog.Error(err.Error())
72 | os.Exit(4)
73 | }
74 | }
75 |
76 | opts.WriteOpStatusCodeXlsx = strings.TrimSpace(opts.WriteOpStatusCodeXlsx)
77 | if len(opts.WriteOpStatusCodeXlsx) > 0 {
78 | err := sm.WriteFileXLSXOperationStatusCodes(
79 | opts.WriteOpStatusCodeXlsx)
80 | if err != nil {
81 | slog.Error(err.Error())
82 | os.Exit(5)
83 | }
84 | }
85 |
86 | opIDs := sm.OperationIDs()
87 | slog.Info("operationIDs", "count", len(opIDs))
88 |
89 | endpoints := sm.PathMethods(true)
90 | slog.Info("endpoints", "count", len(endpoints))
91 |
92 | slog.Info("DONE")
93 | os.Exit(0)
94 | }
95 |
--------------------------------------------------------------------------------
/openapi2/copy.go:
--------------------------------------------------------------------------------
1 | package openapi2
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 |
7 | "github.com/qdm12/reprint"
8 | )
9 |
10 | func CopyEndpointsByTag(tag string, specOld, specNew Specification) (Specification, error) {
11 | var err error
12 | for url, path := range specOld.Paths {
13 | if path.Delete != nil {
14 | specNew, err = copyOrIgnoreEndpoint(http.MethodDelete, *path.Delete, url, path, tag, specNew)
15 | if err != nil {
16 | return specNew, err
17 | }
18 | }
19 | if path.Get != nil {
20 | specNew, err = copyOrIgnoreEndpoint(http.MethodGet, *path.Get, url, path, tag, specNew)
21 | if err != nil {
22 | return specNew, err
23 | }
24 | }
25 | if path.Head != nil {
26 | specNew, err = copyOrIgnoreEndpoint(http.MethodHead, *path.Head, url, path, tag, specNew)
27 | if err != nil {
28 | return specNew, err
29 | }
30 | }
31 | if path.Options != nil {
32 | specNew, err = copyOrIgnoreEndpoint(http.MethodOptions, *path.Options, url, path, tag, specNew)
33 | if err != nil {
34 | return specNew, err
35 | }
36 | }
37 | if path.Patch != nil {
38 | specNew, err = copyOrIgnoreEndpoint(http.MethodPatch, *path.Patch, url, path, tag, specNew)
39 | if err != nil {
40 | return specNew, err
41 | }
42 | }
43 | if path.Post != nil {
44 | specNew, err = copyOrIgnoreEndpoint(http.MethodPost, *path.Post, url, path, tag, specNew)
45 | if err != nil {
46 | return specNew, err
47 | }
48 | }
49 | if path.Put != nil {
50 | specNew, err = copyOrIgnoreEndpoint(http.MethodPut, *path.Put, url, path, tag, specNew)
51 | if err != nil {
52 | return specNew, err
53 | }
54 | }
55 | }
56 | return specNew, nil
57 | }
58 |
59 | func copyOrIgnoreEndpoint(method string, endpoint Endpoint, url string, path Path, wantTag string, specNew Specification) (Specification, error) {
60 | // TODO: copy referenced objects, e.g. schema objects, which may need `specOld`.
61 | wantTag = strings.TrimSpace(wantTag)
62 | if len(wantTag) != 0 {
63 | match := false
64 | for _, tryTag := range endpoint.Tags {
65 | if strings.TrimSpace(tryTag) == wantTag {
66 | match = true
67 | }
68 | }
69 | if !match {
70 | return specNew, nil
71 | }
72 | }
73 | if _, ok := specNew.Paths[url]; ok {
74 | return specNew, nil
75 | }
76 |
77 | pathNew := reprint.This(path).(Path) // ref: https://stackoverflow.com/a/77412997/1908967
78 | err := pathNew.SetEndpoint(method, endpoint)
79 | if err != nil {
80 | return specNew, err
81 | }
82 | if specNew.Paths == nil {
83 | specNew.Paths = map[string]Path{}
84 | }
85 | specNew.Paths[url] = pathNew
86 | return specNew, nil
87 | }
88 |
--------------------------------------------------------------------------------
/openapi3/path.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "net/http"
5 | "regexp"
6 | "sort"
7 |
8 | oas3 "github.com/getkin/kin-openapi/openapi3"
9 | )
10 |
11 | var RxPathParam = regexp.MustCompile(`\{([^\{\}]+)\}`)
12 |
13 | func PathParams(p string) []string {
14 | params := []string{}
15 | m := RxPathParam.FindAllStringSubmatch(p, -1)
16 | for _, mi := range m {
17 | params = append(params, mi[1])
18 | }
19 | return params
20 | }
21 |
22 | func PathItemHasEndpoints(pathItem *oas3.PathItem) bool {
23 | if pathItem.Connect != nil || pathItem.Delete != nil ||
24 | pathItem.Get != nil || pathItem.Head != nil ||
25 | pathItem.Options != nil ||
26 | pathItem.Patch != nil || pathItem.Post != nil ||
27 | pathItem.Put != nil || pathItem.Trace != nil {
28 | return true
29 | }
30 | return false
31 | }
32 |
33 | func PathMethods(pathItem *oas3.PathItem) []string {
34 | methods := []string{}
35 | if pathItem.Connect != nil {
36 | methods = append(methods, http.MethodConnect)
37 | }
38 | if pathItem.Delete != nil {
39 | methods = append(methods, http.MethodDelete)
40 | }
41 | if pathItem.Get != nil {
42 | methods = append(methods, http.MethodGet)
43 | }
44 | if pathItem.Head != nil {
45 | methods = append(methods, http.MethodHead)
46 | }
47 | if pathItem.Options != nil {
48 | methods = append(methods, http.MethodOptions)
49 | }
50 | if pathItem.Patch != nil {
51 | methods = append(methods, http.MethodPatch)
52 | }
53 | if pathItem.Post != nil {
54 | methods = append(methods, http.MethodPost)
55 | }
56 | if pathItem.Put != nil {
57 | methods = append(methods, http.MethodPut)
58 | }
59 | if pathItem.Trace != nil {
60 | methods = append(methods, http.MethodTrace)
61 | }
62 | return methods
63 | }
64 |
65 | var rxPathVarToGeneric = regexp.MustCompile(`{[^}{]*}`)
66 |
67 | func PathVarsToGeneric(input string) string {
68 | return rxPathVarToGeneric.ReplaceAllString(input, "{}")
69 | }
70 |
71 | func PathMatchGeneric(path1, path2 string) bool {
72 | return PathVarsToGeneric(path1) == PathVarsToGeneric(path2)
73 | }
74 |
75 | func (sm *SpecMore) PathMethods(generic bool) []string {
76 | endpoints := []string{}
77 | if sm.Spec == nil {
78 | return endpoints
79 | }
80 | pathsMap := sm.Spec.Paths.Map()
81 | for url, pathItem := range pathsMap {
82 | // for url, pathItem := range sm.Spec.Paths { // getkin v0.121.0 to v0.122.0
83 | if generic {
84 | url = PathVarsToGeneric(url)
85 | }
86 | pathMethods := PathMethods(pathItem)
87 | for _, pathMethod := range pathMethods {
88 | endpoints = append(endpoints, url+" "+pathMethod)
89 | }
90 | }
91 | sort.Strings(endpoints)
92 | return endpoints
93 | }
94 |
--------------------------------------------------------------------------------
/openapi3/spec_meta.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 |
7 | oas3 "github.com/getkin/kin-openapi/openapi3"
8 | "github.com/grokify/mogo/os/osutil"
9 | )
10 |
11 | // NewSpec returns a new OpenAPI 3 spec that will validate.
12 | // Specifically, it includes an OAS version, sets `info` to
13 | // be an empty object instead of null and sets apiVersion.
14 | func NewSpec(oasVersion, apiTitle, apiVersion string) *Spec {
15 | oasVersion = strings.TrimSpace(oasVersion)
16 | if len(oasVersion) == 0 {
17 | oasVersion = OASVersionLatest
18 | }
19 | apiVersion = strings.TrimSpace(apiVersion)
20 | if len(apiVersion) == 0 {
21 | apiVersion = apiVersionDefault
22 | }
23 | return &Spec{
24 | OpenAPI: oasVersion,
25 | Info: &oas3.Info{
26 | Title: strings.TrimSpace(apiTitle),
27 | Version: apiVersion}}
28 | }
29 |
30 | type SpecMetas struct {
31 | Metas []SpecMeta
32 | }
33 |
34 | func (metas *SpecMetas) Filepaths(validOnly bool) []string {
35 | files := []string{}
36 | for _, meta := range metas.Metas {
37 | if validOnly && !meta.IsValid {
38 | continue
39 | }
40 | meta.Filepath = strings.TrimSpace(meta.Filepath)
41 | if len(meta.Filepath) > 0 {
42 | files = append(files, meta.Filepath)
43 | }
44 | }
45 | return files
46 | }
47 |
48 | type SpecMeta struct {
49 | Filepath string
50 | Version int
51 | IsValid bool
52 | ValidationError string
53 | }
54 |
55 | func ReadSpecMetasDir(dir string, rx *regexp.Regexp) (SpecMetas, error) {
56 | metas := SpecMetas{Metas: []SpecMeta{}}
57 | if entries, err := osutil.ReadDirMore(dir, rx, false, true, false); err != nil {
58 | return metas, err
59 | } else {
60 | return ReadSpecMetasFiles(entries.Names(dir))
61 | }
62 | }
63 |
64 | func ReadSpecMetasFiles(files []string) (SpecMetas, error) {
65 | metas := SpecMetas{Metas: []SpecMeta{}}
66 | for _, f := range files {
67 | _, err := ReadFile(f, true)
68 | meta := SpecMeta{
69 | Filepath: f,
70 | Version: 3}
71 | if err != nil {
72 | meta.ValidationError = err.Error()
73 | } else {
74 | meta.IsValid = true
75 | }
76 | metas.Metas = append(metas.Metas, meta)
77 | }
78 | return metas, nil
79 | }
80 |
81 | func (metas *SpecMetas) Merge(validatesOnly bool, mergeOpts *MergeOptions) (SpecMore, error) {
82 | return MergeSpecMetas(metas, validatesOnly, mergeOpts)
83 | }
84 |
85 | func MergeSpecMetas(metas *SpecMetas, validatesOnly bool, mergeOpts *MergeOptions) (SpecMore, error) {
86 | specMore := SpecMore{}
87 | filepaths := metas.Filepaths(validatesOnly)
88 | spec, err := MergeFiles(filepaths, mergeOpts)
89 | if err != nil {
90 | return specMore, err
91 | }
92 | specMore.Spec = spec
93 | return specMore, nil
94 | }
95 |
--------------------------------------------------------------------------------
/openapi3/ontology/tag_ontology_test.go:
--------------------------------------------------------------------------------
1 | package ontology
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/grokify/mogo/text/stringcase"
9 | )
10 |
11 | var onologyTestStruct = Ontology{
12 | OperationIDCase: stringcase.KebabCase,
13 | SchemaNameCase: stringcase.SnakeCase,
14 | SchemaNameReponseSuffix: "response",
15 | PathVarCase: stringcase.SnakeCase,
16 | PathIDPrefix: "",
17 | PathIDSuffix: "id",
18 | SpecFileCase: stringcase.SnakeCase,
19 | SpecFilePrefix: "spec",
20 | SpecFileExt: ".yaml",
21 | SpecFileResourceIsPlural: false,
22 | }
23 |
24 | var tagOnologyTests = []struct {
25 | tagOntology TagOnology
26 | tagOntologyData TagOnologyData
27 | }{
28 | {
29 | tagOntology: TagOnology{
30 | ResourceNameSingular: "pet animal",
31 | ResourceNameSingularTitle: "Pet Animal",
32 | ResourceNameSingularShort: "pet",
33 | ResourceNamePlural: "pet animals",
34 | ResourceNamePluralTitle: "Pet Animals",
35 | DeterminerSinglar: "a",
36 | DeterminerPlural: "",
37 | },
38 | tagOntologyData: TagOnologyData{
39 | Tag: "Pet Animals",
40 | CreateOperationID: "create-pet-animal",
41 | ReadOperationID: "get-pet-animal",
42 | UpdateOperationID: "update-pet-animal",
43 | DeleteOperationID: "delete-pet-animal",
44 | ListOperationID: "list-pet-animals",
45 | CreateSummary: "Create a pet animal",
46 | ReadSummary: "Describe a pet animal",
47 | UpdateSummary: "Update a pet animal",
48 | DeleteSummary: "Delete a pet animal",
49 | ListSummary: "List pet animals",
50 | ResourcePathVar: "pet_id",
51 | ResourceSchemaNameRequest: "pet_animal",
52 | ResourceSchemaNameResponse: "pet_animal_response",
53 | SpecFilename: "spec_pet_animal.yaml",
54 | },
55 | },
56 | }
57 |
58 | // TestParseJSONPointer ensures the `ParseJSONPointer` is working properly.
59 | func TestTagOnologyTests(t *testing.T) {
60 | for _, tt := range tagOnologyTests {
61 | to := tt.tagOntology
62 | to.Ontology = onologyTestStruct
63 | gotData := to.Data()
64 | wantData := tt.tagOntologyData
65 | if gotData.CreateSummary != wantData.CreateSummary {
66 | fmt.Printf("MISMATCH_SUMMARIES:\nwant [%s]\nrecv [%s]\n", wantData.CreateSummary, gotData.CreateSummary)
67 | }
68 | mismatchFields, isEqual := gotData.Equal(wantData)
69 | if !isEqual {
70 | t.Errorf("ontology.TagOnologyData.Data(...) Mistmatch fields [%s] want [%v] got [%v]",
71 | strings.Join(mismatchFields, ","),
72 | wantData, gotData)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/docs/recommendations_openapi3.md:
--------------------------------------------------------------------------------
1 | # OpenAPI 3 Spec
2 |
3 | This document is a recommended set of minimal OpenAPI 3 spec properties so that a spec can be used by various ecosystem tools such as API References, API Explorers and Client SDK generators.
4 |
5 | ## Operation
6 |
7 | | Property | Requirement | Notes |
8 | |----------|-------------|-------|
9 | | `description` | `MUST` | Used by API References |
10 | | `operationId` | `MUST` | May be used to auto-generate client SDK method names |
11 | | `responses` | `MUST` | Minimally must have 2xx successful response. Other responses including errors are desirable |
12 | | `summary` | `MUST` | Used in API References, such as Swagger UI and ReadMe.io |
13 | | `tags` | `MUST` | There should have 1 and only 1 tag. Tags are used to organize endpoints in API References and Client SDKs. More than 1 tag may not be supported will in some software |
14 |
15 | ### Operation Parameter
16 |
17 | | Property | Requirement | Notes |
18 | |----------|-------------|-------|
19 | | `in` | `MUST` | This describes where the parameter appears. |
20 | | `name` | `MUST` | This is the name of the parameter. |
21 | | `required` | `MUST` | This is the name of the parameter. |
22 | | `description` | `SHOULD` | This describes where the parameter appears. |
23 | | `schema.type` or `schema.$ref` | `MUST` | Type property must be present. Schema paramters are typically not objects which would be defined by a `$ref`, though JSON bodes are. |
24 | | `schema.format` | `SHOULD` | Format property should be present. For `integer` type, if using `long`, explicitly set `format` to `int64`. For Date Time properties, only set `format` to `date-time` or `date` if the fields correspond to IETF RFC-3339. If date/time formats do not correspond to RFC-3339, leave `format` empty and add format information in the `description` property |
25 |
26 | ## Schema
27 |
28 | | Property | Requirement | Notes |
29 | |----------|-------------|-------|
30 | | `required` | `MUST` | When required fields are present, they should be indicated |
31 |
32 | ### Schema Property
33 |
34 | | Property | Requirement | Notes |
35 | |----------|-------------|-------|
36 | | `description` | `SHOULD` | Description should be included when available. For fields with ambiguous `format` information such as non-RFC-3339 date/times, the format should be included in the description. |
37 | | `type` or `$ref` | `MUST` | Type property must be present. |
38 | | `format` | `SHOULD` | Format property should be present. For `integer` type, if using `long`, explicitly set `format` to `int64`. For Date Time properties, only set `format` to `date-time` or `date` if the fields correspond to IETF RFC-3339. If date/time formats do not correspond to RFC-3339, leave `format` empty and add format information in the `description` property |
--------------------------------------------------------------------------------
/openapi3lint/README.md:
--------------------------------------------------------------------------------
1 | # OpenAPI 3 Lint
2 |
3 | This is a Go-based OpenAPI 3 spec linter.
4 |
5 | Spectrum is designed to support a multi-file, multi-user, async editing process where linting reports need to be resilinent to mutiple changes to the specs occurring between the time the validation is run and resolved.
6 |
7 | ## Why Spectrum Linter?
8 |
9 | There are a few linters available.
10 |
11 | The reasons this exists are:
12 |
13 | 1. written in Go so its easy to use/modify for Go devs
14 | 2. policy violations are grouped by rule, vs. line number for easier mitigation
15 | 3. policy violations are identified by JSON Schema pointere vs. line number for easier identification when using merged files
16 |
17 | ## Standard Rules
18 |
19 | The following rules are built into Spectrum.
20 |
21 | 1. `datatype-int-format-standard-exist`: ensures data types of `integer` have a standard format (`int32` or `int64`)
22 | 1. `operation-operationid-style-camelcase`: ensures operationIds use camelCase
23 | 1. `operation-operationid-style-kebabcase`: ensures operationIds use kebab-case
24 | 1. `operation-operationid-style-pascalcase`: ensures operationIds use PascalCase
25 | 1. `operation-operationid-style-snakecase`: ensures operationIds use snake_case
26 | 1. `operation-summary-exist` ensures a summary exists.
27 | 1. `operation-summary-style-first-uppercase`: ensures summary starts with capitalized first character
28 | 1. `path-param-style-camelcase`: path parms are camel case
29 | 1. `path-param-style-kebabcase`: path parms are kebab case
30 | 1. `path-param-style-pascalcase`: path parms are Pascal case
31 | 1. `path-param-style-snakecase`: path parms are snake case
32 | 1. `schema-has-reference`: ensures schemas have references
33 | 1. `schema-object-properties-exist`: schema of type `object` have `properties` or `additionalProperties` defined
34 | 1. `schema-property-enum-style-camelcase`: schema property enums are camel case
35 | 1. `schema-property-enum-style-kebabcase`: schema property enums are kebab case
36 | 1. `schema-property-enum-style-pascalcase`: schema property enums are Pascal case
37 | 1. `schema-property-enum-style-snakecase`: schema property enums are snake case
38 | 1. `schema-reference-has-schema`: ensures schma JSON pointers reference existing schemas
39 | 1. `tag-style-first-uppercase`: Tag names have capitalized first character
40 |
41 | ## Other Linters
42 |
43 | There are other linters available. To date, Spectrum Linter hasn't beeen inspired by any of them, though there is a desire and effort to align on rule names and potentially rule definitions to achieve similar behavior.
44 |
45 | 1. Mermade OAS-Kit - https://github.com/mermade/oas-kit
46 | 1. https://mermade.github.io/oas-kit/default-rules.html
47 | 1. https://mermade.github.io/oas-kit/linter-rules.html
48 | 1. Spectral - https://github.com/stoplightio/spectral
49 | 1. Inspired by Speccy
50 | 1. Speccy - https://github.com/wework/speccy
51 | 1. Inspired by OAS-Kit
--------------------------------------------------------------------------------
/openapi3/schemas.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "os"
5 | "strings"
6 |
7 | oas3 "github.com/getkin/kin-openapi/openapi3"
8 | "github.com/grokify/mogo/encoding/jsonpointer"
9 | "github.com/grokify/mogo/type/stringsutil"
10 | )
11 |
12 | func SchemaPointerExpand(prefix, schemaName string) string {
13 | // https://swagger.io/docs/specification/components/
14 | prefix = strings.TrimSpace(prefix)
15 | schemaName = strings.TrimSpace(schemaName)
16 | pointer := schemaName
17 | if !strings.Contains(schemaName, PointerComponentsSchemas) {
18 | pointer = PointerComponentsSchemas + "/" + jsonpointer.PropertyNameEscape(schemaName)
19 | }
20 | if len(prefix) > 0 && strings.Index(pointer, "#") == 0 {
21 | pointer = prefix + pointer
22 | }
23 | return pointer
24 | }
25 |
26 | func ReadSchemaFile(filename string) (*oas3.Schema, error) {
27 | data, err := os.ReadFile(filename)
28 | if err != nil {
29 | return nil, err
30 | }
31 | sch := oas3.NewSchema()
32 | err = sch.UnmarshalJSON(data)
33 | return sch, err
34 | }
35 |
36 | // AdditionalPropertiesAllowed checks for additional properties, which exists in Schema structs.
37 | func AdditionalPropertiesAllowed(aprops oas3.AdditionalProperties) bool {
38 | if aprops.Has != nil {
39 | return *aprops.Has
40 | } else {
41 | return false
42 | }
43 | }
44 |
45 | func AdditionalPropertiesExists(props oas3.AdditionalProperties) bool {
46 | if props.Has == nil || !*props.Has || props.Schema == nil {
47 | return false
48 | } else if strings.TrimSpace(props.Schema.Ref) != "" {
49 | return true
50 | } else if props.Schema.Value == nil {
51 | return false
52 | } else {
53 | return true
54 | }
55 | }
56 |
57 | // NewTypes returns a value that is suitable for `oas3.Schema.Value.Type`.
58 | // In `0.123.0` and earlier, this is a `string`. In `0.124.0` and later, this is
59 | // of type `oas3.Types`. `oas3` is `github.com/getkin/kin-openapi/openapi3`.
60 | func NewTypesRef(t ...string) *oas3.Types {
61 | ot := oas3.Types{}
62 | for _, ti := range t {
63 | ot = append(ot, ti)
64 | }
65 | return &ot
66 | }
67 |
68 | // TypesRefIs returns if the supplied `*oas3.Types` is any of the supplied values.
69 | // It returns false if `*oas3.Types` is false, or `type` an empty slice, or
70 | // none of the supplied types match. `oas3` is `github.com/getkin/kin-openapi/openapi3`.
71 | func TypesRefIs(t *oas3.Types, types ...string) bool {
72 | types = stringsutil.SliceCondenseSpace(types, true, false)
73 | if t == nil || len(*t) == 0 {
74 | if len(types) == 0 {
75 | return true
76 | } else {
77 | return false
78 | }
79 | } else if len(types) == 0 {
80 | return false
81 | }
82 | for _, typ := range types {
83 | if t.Is(typ) {
84 | return true
85 | }
86 | }
87 | return false
88 | }
89 |
90 | // TypesRefString returns a string if `oas3.Types` is not nil and has a single type.
91 | func TypesRefString(t *oas3.Types) string {
92 | if t == nil || len(*t) != 1 {
93 | return ""
94 | } else {
95 | return (*t)[0]
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/openapi3/springopenapi3/spring_parser_test.go:
--------------------------------------------------------------------------------
1 | package springopenapi3
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/grokify/spectrum/openapi3"
7 | )
8 |
9 | var parseLineTests = []struct {
10 | v string
11 | oasName string
12 | oasType string
13 | oasFormat string
14 | oasDefault interface{}
15 | explicitCustomTypes []string
16 | }{
17 | {"private Boolean myPropBoolean;", "myPropBoolean", "boolean", "", nil, []string{}},
18 | {"private DateTime myPropDateTime;", "myPropDateTime", "string", "", nil, []string{}},
19 | {"private Integer myPropInteger = 1;", "myPropInteger", "integer", "", 1, []string{}},
20 | {"private Long myPropLong = 1;", "myPropLong", "integer", "int64", 1, []string{}},
21 | {"private String myPropString;", "myPropString", "string", "", nil, []string{}},
22 | {"private String myPropString = \"\";", "myPropString", "string", "", "", []string{}},
23 | {"private String myPropString = \"AVAILABLE\";", "myPropString", "string", "", "AVAILABLE", []string{}},
24 | {"private List myPropStrings = new ArrayList<>();", "myPropStrings", "array", "", nil, []string{}},
25 | {"private List myPropStrings = new ArrayList<>();", "myPropStrings", "array", "", nil, []string{}},
26 | {"private List myPropStrings;", "myPropStrings", "array", "", nil, []string{}},
27 | {"private List myPropStrings;", "myPropStrings", "array", "", nil, []string{}},
28 | }
29 |
30 | func TestParseLine(t *testing.T) {
31 | for _, tt := range parseLineTests {
32 | name, schemaRef, err := ParseSpringLineToSchemaRef(tt.v, tt.explicitCustomTypes)
33 | if err != nil {
34 | t.Errorf("fromspring.ParseSpringLineToSchema() [%v]", err)
35 | }
36 | schema := schemaRef.Value
37 | if tt.oasName != name || tt.oasType != openapi3.TypesRefString(schema.Type) || tt.oasFormat != schema.Format {
38 | t.Errorf(`fromspring.ParseSpringLineToSchema("%s") MISMATCH W[%v]G[%v] [%v][%v] [%v][%v]`, tt.v, tt.oasName, name, tt.oasType, schema.Type, tt.oasFormat, schema.Format)
39 | }
40 | //fmtutil.PrintJSON(schema)
41 | }
42 | }
43 |
44 | /*
45 | const CampaignLeadSearchCriteriaSimple = `private List leadIds = new ArrayList<>();
46 | private List listIds = new ArrayList<>();
47 | private List externIds = new ArrayList<>();
48 | private List physicalStates;
49 | private List agentDispositions;
50 | private List leadPhoneNumbers = new ArrayList<>();
51 | private boolean orphanedLeadsOnly;
52 | private String callerId;
53 | private String leadPhoneNum;
54 | private List campaignIds = new ArrayList<>();
55 | private String firstName;
56 | private String lastName;
57 | private String address1;
58 | private String address2;
59 | private String city;
60 | private String zip;
61 | private String emailAddress;
62 | private String auxData1;
63 | private String auxData2;
64 | private String auxData3;
65 | private String auxData4;
66 | private String auxData5;
67 | private Integer pendingAgentId;
68 | private Integer agentId;`
69 | */
70 |
--------------------------------------------------------------------------------
/openapi3/openapi3html/pageparams.go:
--------------------------------------------------------------------------------
1 | package openapi3html
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "html"
7 | "os"
8 | "strings"
9 |
10 | oas3 "github.com/getkin/kin-openapi/openapi3"
11 | "github.com/grokify/gocharts/v2/data/table"
12 | "github.com/grokify/gocharts/v2/data/table/tabulator"
13 | "github.com/grokify/spectrum/openapi3"
14 | )
15 |
16 | type PageParams struct {
17 | PageTitle string
18 | PageLink string
19 | TableDomID string
20 | Spec *openapi3.Spec
21 | ColumnSet *tabulator.ColumnSet
22 | OpsFilterFunc func(path, method string, op *oas3.Operation) bool
23 | OpsAdditionalFormatFuncs *openapi3.OperationMoreStringFuncMap
24 | TableJSON []byte
25 | }
26 |
27 | func (pp *PageParams) PageLinkHTML() string {
28 | pp.PageLink = strings.TrimSpace(pp.PageLink)
29 | if len(pp.PageLink) == 0 {
30 | return html.EscapeString(pp.PageTitle)
31 | }
32 | return fmt.Sprintf("%s", pp.PageLink,
33 | html.EscapeString(pp.PageTitle))
34 | }
35 |
36 | func (pp *PageParams) AddSpec(spec *openapi3.Spec) error {
37 | sm := openapi3.SpecMore{Spec: spec}
38 | tbl, err := sm.OperationsTable(pp.ColumnSet, pp.OpsFilterFunc, pp.OpsAdditionalFormatFuncs)
39 | if err != nil {
40 | return err
41 | }
42 | return pp.AddOperationsTable(tbl)
43 | }
44 |
45 | func (pp *PageParams) AddOperationsTable(tbl *table.Table) error {
46 | docs := tbl.ToDocuments()
47 | jdocs, err := json.Marshal(docs)
48 | if err != nil {
49 | return err
50 | }
51 | pp.TableJSON = jdocs
52 | return nil
53 | }
54 |
55 | func (pp *PageParams) TableJSONBytesOrEmpty() []byte {
56 | empty := []byte("[]")
57 | if len(pp.TableJSON) > 0 {
58 | return pp.TableJSON
59 | }
60 | if pp.Spec != nil {
61 | err := pp.AddSpec(pp.Spec)
62 | if err != nil {
63 | return empty
64 | }
65 | return pp.TableJSON
66 | }
67 | return empty
68 | }
69 |
70 | func (pp *PageParams) TabulatorColumnsJSONBytesOrEmpty() []byte {
71 | if pp.ColumnSet == nil || len(pp.ColumnSet.Columns) == 0 {
72 | colSet := openapi3.OpTableColumnsDefault(false)
73 | tcols := tabulator.BuildColumnsTabulator(colSet.Columns)
74 | return tcols.MustColumnsJSON()
75 | }
76 | tcols := tabulator.BuildColumnsTabulator(pp.ColumnSet.Columns)
77 | return tcols.MustColumnsJSON()
78 | }
79 |
80 | func (pp *PageParams) WriteFile(filename string) error {
81 | f, err := os.Create(filename)
82 | if err != nil {
83 | return err
84 | }
85 | defer f.Close()
86 | WriteSpectrumUIPage(f, *pp)
87 | return nil
88 | }
89 |
90 | /*
91 | func DefaultColumns() text.Texts {
92 | return text.Texts{
93 | {
94 | Display: "Method",
95 | Slug: "method"},
96 | {
97 | Display: "Path",
98 | Slug: "path"},
99 | {
100 | Display: "OperationID",
101 | Slug: "operationId"},
102 | {
103 | Display: "Summary",
104 | Slug: "summary"},
105 | {
106 | Display: "Tags",
107 | Slug: "tags"},
108 | }
109 | }
110 | */
111 |
--------------------------------------------------------------------------------
/ext/taxonomy/categories.go:
--------------------------------------------------------------------------------
1 | package taxonomy
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 |
8 | mapslice "github.com/ake-persson/mapslice-json"
9 | oas3 "github.com/getkin/kin-openapi/openapi3"
10 | "github.com/grokify/mogo/encoding/jsonutil"
11 | "github.com/grokify/mogo/errors/errorsutil"
12 | "github.com/grokify/spectrum/openapi3"
13 | "sigs.k8s.io/yaml"
14 | )
15 |
16 | const XTaxonomy = "x-taxonomy"
17 |
18 | type Category struct {
19 | Key string `json:"-"`
20 | Slug string `json:"slug,omitempty"`
21 | Title string `json:"title,omitempty"`
22 | Description string `json:"description,omitempty"`
23 | Tags []oas3.Tag `json:"tags,omitempty" yaml:"tags,omitempty"`
24 | }
25 |
26 | func (cat *Category) Taxonomy(catsFilepath string) (Taxonomy, error) {
27 | if len(cat.Key) == 0 {
28 | return Taxonomy{}, errors.New("categgory `Key` not set")
29 | }
30 | return Taxonomy{
31 | Category: CategoryRef{Ref: fmt.Sprintf("%s#/%s", catsFilepath, cat.Key)},
32 | Slug: cat.Slug}, nil
33 | }
34 |
35 | type Categories []Category
36 |
37 | var ErrCategoryNotFound = errors.New("category not found")
38 |
39 | // SpecAddXTaxonomy expects `catTitle` to be the same as the OAS3 `tag` name.
40 | func (cats *Categories) SpecAddXTaxonomy(spec *openapi3.Spec, catTitle, catsFilepath string) error {
41 | if spec == nil {
42 | return openapi3.ErrSpecNotSet
43 | }
44 | cat, err := cats.Category(catTitle)
45 | if err != nil {
46 | return errorsutil.Wrapf(ErrCategoryNotFound, "category (%s)", catTitle)
47 | }
48 | tax, err := cat.Taxonomy(catsFilepath)
49 | if err != nil {
50 | return err
51 | }
52 | tax.AddToSpec(spec)
53 | return nil
54 | }
55 |
56 | func (cats *Categories) Category(title string) (Category, error) {
57 | for _, cat := range *cats {
58 | if cat.Title == title {
59 | return cat, nil
60 | }
61 | }
62 | return Category{}, errorsutil.Wrapf(ErrCategoryNotFound, "category (%s)", title)
63 | }
64 |
65 | func (cats *Categories) MapSlice() mapslice.MapSlice {
66 | ms := mapslice.MapSlice{}
67 | for _, cat := range *cats {
68 | ms = append(ms, mapslice.MapItem{Key: cat.Key, Value: cat})
69 | }
70 | return ms
71 | }
72 |
73 | func (cats *Categories) MarshalJSON(prefix, indent string) ([]byte, error) {
74 | return jsonutil.MarshalSimple(cats.MapSlice(), prefix, indent)
75 | }
76 |
77 | func (cats *Categories) MarshalYAML() ([]byte, error) {
78 | if jbytes, err := cats.MarshalJSON("", ""); err != nil {
79 | return []byte{}, err
80 | } else {
81 | return yaml.JSONToYAML(jbytes)
82 | }
83 | }
84 |
85 | func (cats *Categories) WriteFileJSON(filename string, perm os.FileMode, prefix, indent string) error {
86 | if jsonData, err := cats.MarshalJSON(prefix, indent); err != nil {
87 | return err
88 | } else {
89 | return os.WriteFile(filename, jsonData, perm)
90 | }
91 | }
92 |
93 | func (cats *Categories) WriteFileYAML(filename string, perm os.FileMode) error {
94 | if ybytes, err := cats.MarshalYAML(); err != nil {
95 | return err
96 | } else {
97 | return os.WriteFile(filename, ybytes, perm)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/openapi3lint/ruleschemapropenumstyle/rule_schema_prop_enum_style.go:
--------------------------------------------------------------------------------
1 | package ruleschemapropenumstyle
2 |
3 | import (
4 | "fmt"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/encoding/jsonpointer"
8 | "github.com/grokify/mogo/text/stringcase"
9 | "github.com/grokify/spectrum/openapi3"
10 | "github.com/grokify/spectrum/openapi3lint/lintutil"
11 | )
12 |
13 | type RuleSchemaPropEnumStyle struct {
14 | name string
15 | stringCase string
16 | }
17 |
18 | func NewRule(requiredStringCase string) (RuleSchemaPropEnumStyle, error) {
19 | canonicalCase, err := stringcase.Parse(requiredStringCase)
20 | if err != nil {
21 | return RuleSchemaPropEnumStyle{},
22 | fmt.Errorf("invalid string case [%s]", requiredStringCase)
23 | }
24 | rule := RuleSchemaPropEnumStyle{
25 | stringCase: canonicalCase}
26 | switch canonicalCase {
27 | case stringcase.CamelCase:
28 | rule.name = lintutil.RulenameSchemaPropEnumStyleCamelCase
29 | case stringcase.KebabCase:
30 | rule.name = lintutil.RulenameSchemaPropEnumStyleKebabCase
31 | case stringcase.PascalCase:
32 | rule.name = lintutil.RulenameSchemaPropEnumStylePascalCase
33 | case stringcase.SnakeCase:
34 | rule.name = lintutil.RulenameSchemaPropEnumStyleSnakeCase
35 | default:
36 | return rule, fmt.Errorf("invalid string case [%s]", canonicalCase)
37 | }
38 | return rule, nil
39 | }
40 |
41 | func (rule RuleSchemaPropEnumStyle) Name() string {
42 | return rule.name
43 | }
44 |
45 | func (rule RuleSchemaPropEnumStyle) Scope() string {
46 | return lintutil.ScopeSpecification
47 | }
48 |
49 | func (rule RuleSchemaPropEnumStyle) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
50 | return nil
51 | }
52 |
53 | func (rule RuleSchemaPropEnumStyle) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
54 | vios := []lintutil.PolicyViolation{}
55 |
56 | for schName, schRef := range spec.Components.Schemas {
57 | if schRef == nil || schRef.Value == nil || !openapi3.TypesRefIs(schRef.Value.Type, openapi3.TypeObject) {
58 | continue
59 | }
60 |
61 | for propName, propRef := range schRef.Value.Properties {
62 | if propRef.Value == nil || !openapi3.TypesRefIs(propRef.Value.Type, openapi3.TypeString) ||
63 | len(propRef.Value.Enum) == 0 {
64 | continue
65 | }
66 | for i, enumValue := range propRef.Value.Enum {
67 | if enumValueString, ok := enumValue.(string); ok {
68 | jsPtr := jsonpointer.PointerSubEscapeAll(
69 | "%s#/components/schemas/%s/properties/%s/%d",
70 | pointerBase, schName, propName, i)
71 | isWantCase, err := stringcase.IsCase(rule.stringCase, enumValueString)
72 | if err != nil {
73 | // should never happen as rule.stringCase should be validated.
74 | vios = append(vios, lintutil.PolicyViolation{
75 | RuleName: rule.Name(),
76 | Location: jsPtr,
77 | Value: enumValueString})
78 | } else if !isWantCase {
79 | vios = append(vios, lintutil.PolicyViolation{
80 | RuleName: rule.Name(),
81 | Location: jsPtr,
82 | Value: enumValueString})
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | return vios
90 | }
91 |
--------------------------------------------------------------------------------
/openapi2/merge.go:
--------------------------------------------------------------------------------
1 | package openapi2
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "regexp"
8 | "strings"
9 |
10 | "github.com/grokify/mogo/errors/errorsutil"
11 | "github.com/grokify/mogo/os/osutil"
12 | )
13 |
14 | var jsonFileRx = regexp.MustCompile(`(?i)\.json\s*$`)
15 |
16 | func MergeDirectory(dir string) (Specification, error) {
17 | entries, err := osutil.ReadDirMore(dir, jsonFileRx, false, true, false)
18 | if err != nil {
19 | return Specification{}, err
20 | }
21 | if len(entries) == 0 {
22 | return Specification{}, fmt.Errorf("no JSON files found in directory [%s]", dir)
23 | }
24 | var specMaster Specification
25 | for i, entry := range entries {
26 | thisSpecFilepath := filepath.Join(dir, entry.Name())
27 | thisSpec, err := ReadOpenAPI2SpecFileDirect(thisSpecFilepath)
28 | if err != nil {
29 | return specMaster, err
30 | }
31 | if i == 0 {
32 | specMaster = thisSpec
33 | } else {
34 | specMaster = Merge(specMaster, thisSpec)
35 | }
36 | }
37 | return specMaster, nil
38 | }
39 |
40 | func MergeFilepaths(filepaths []string) (Specification, error) {
41 | var specMaster Specification
42 | for i, fpath := range filepaths {
43 | fmt.Printf("[%v][%v]\n", i, fpath)
44 | thisSpec, err := ReadOpenAPI2SpecFileDirect(fpath)
45 | if err != nil {
46 | return specMaster, errorsutil.Wrap(err, fmt.Sprintf("E_READ_SPEC [%v]", fpath))
47 | }
48 | if i == 0 {
49 | specMaster = thisSpec
50 | } else {
51 | specMaster = Merge(specMaster, thisSpec)
52 | }
53 | }
54 | return specMaster, nil
55 | }
56 |
57 | func Merge(specMaster, specExtra Specification) Specification {
58 | specMaster = MergeTags(specMaster, specExtra)
59 | specMaster = MergePaths(specMaster, specExtra)
60 | return MergeDefinitions(specMaster, specExtra)
61 | }
62 |
63 | func MergeTags(specMaster, specExtra Specification) Specification {
64 | tagsMap := map[string]int{}
65 | for _, tag := range specMaster.Tags {
66 | tagsMap[tag.Name] = 1
67 | }
68 | for _, tag := range specExtra.Tags {
69 | tag.Name = strings.TrimSpace(tag.Name)
70 | if _, ok := tagsMap[tag.Name]; !ok {
71 | specMaster.Tags = append(specMaster.Tags, tag)
72 | }
73 | }
74 | return specMaster
75 | }
76 |
77 | func MergePaths(specMaster, specExtra Specification) Specification {
78 | for url, path := range specExtra.Paths {
79 | specMaster.Paths[url] = path
80 | }
81 | return specMaster
82 | }
83 |
84 | func MergeDefinitions(specMaster, specExtra Specification) Specification {
85 | for definitionName, def := range specExtra.Definitions {
86 | if specMaster.Definitions == nil {
87 | specMaster.Definitions = map[string]Definition{}
88 | }
89 | specMaster.Definitions[definitionName] = def
90 | }
91 | return specMaster
92 | }
93 |
94 | func WriteFileDirMerge(outfile, inputDir string, perm os.FileMode) error {
95 | spec, err := MergeDirectory(inputDir)
96 | if err != nil {
97 | return errorsutil.Wrap(err, "E_OPENAPI3_MERGE_DIRECTORY_FAILED")
98 | }
99 |
100 | err = osutil.WriteFileJSON(outfile, spec, perm, "", " ")
101 | if err != nil {
102 | return errorsutil.Wrap(err, "E_OPENAPI3_WRITE_FAILED")
103 | }
104 | return nil
105 | }
106 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/grokify/spectrum
2 |
3 | go 1.24.0
4 |
5 | toolchain go1.24.1
6 |
7 | require (
8 | github.com/ake-persson/mapslice-json v0.0.0-20210720081907-22c8edf57807
9 | github.com/buaazp/fasthttprouter v0.1.1
10 | github.com/getkin/kin-openapi v0.133.0
11 | github.com/grokify/gocharts/v2 v2.26.6
12 | github.com/grokify/mogo v0.72.0
13 | github.com/grokify/sogo v0.12.12
14 | github.com/jessevdk/go-flags v1.6.1
15 | github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494
16 | github.com/rs/zerolog v1.34.0
17 | github.com/valyala/fasthttp v1.68.0
18 | github.com/valyala/quicktemplate v1.8.0
19 | golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
20 | gopkg.in/yaml.v3 v3.0.1
21 | sigs.k8s.io/yaml v1.6.0
22 | )
23 |
24 | require (
25 | github.com/andybalholm/brotli v1.2.0 // indirect
26 | github.com/apex/gateway v1.1.2 // indirect
27 | github.com/aws/aws-lambda-go v1.50.0 // indirect
28 | github.com/clipperhouse/displaywidth v0.6.1 // indirect
29 | github.com/clipperhouse/stringish v0.1.1 // indirect
30 | github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
31 | github.com/fatih/color v1.18.0 // indirect
32 | github.com/go-openapi/jsonpointer v0.22.3 // indirect
33 | github.com/go-openapi/swag/jsonname v0.25.3 // indirect
34 | github.com/gorilla/mux v1.8.1 // indirect
35 | github.com/grokify/base36 v1.0.5 // indirect
36 | github.com/huandu/xstrings v1.5.0 // indirect
37 | github.com/iancoleman/strcase v0.3.0 // indirect
38 | github.com/josharian/intern v1.0.0 // indirect
39 | github.com/json-iterator/go v1.1.12 // indirect
40 | github.com/klauspost/compress v1.18.1 // indirect
41 | github.com/mailru/easyjson v0.9.1 // indirect
42 | github.com/mattn/go-colorable v0.1.14 // indirect
43 | github.com/mattn/go-isatty v0.0.20 // indirect
44 | github.com/mattn/go-runewidth v0.0.19 // indirect
45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
46 | github.com/modern-go/reflect2 v1.0.2 // indirect
47 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
48 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
49 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
50 | github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
51 | github.com/olekukonko/errors v1.1.0 // indirect
52 | github.com/olekukonko/ll v0.1.3 // indirect
53 | github.com/olekukonko/tablewriter v1.1.2 // indirect
54 | github.com/perimeterx/marshmallow v1.1.5 // indirect
55 | github.com/pkg/errors v0.9.1 // indirect
56 | github.com/richardlehane/mscfb v1.0.4 // indirect
57 | github.com/richardlehane/msoleps v1.0.4 // indirect
58 | github.com/tiendc/go-deepcopy v1.7.2 // indirect
59 | github.com/valyala/bytebufferpool v1.0.0 // indirect
60 | github.com/woodsbury/decimal128 v1.4.0 // indirect
61 | github.com/xuri/efp v0.0.1 // indirect
62 | github.com/xuri/excelize/v2 v2.10.0 // indirect
63 | github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
64 | go.yaml.in/yaml/v2 v2.4.3 // indirect
65 | golang.org/x/crypto v0.45.0 // indirect
66 | golang.org/x/image v0.33.0 // indirect
67 | golang.org/x/net v0.47.0 // indirect
68 | golang.org/x/sys v0.38.0 // indirect
69 | golang.org/x/text v0.31.0 // indirect
70 | gonum.org/v1/gonum v0.16.0 // indirect
71 | )
72 |
--------------------------------------------------------------------------------
/openapi3lint/ruleopxpropertystringexist/rule_op_xproperty_string_exist.go:
--------------------------------------------------------------------------------
1 | package ruleopxpropertystringexist
2 |
3 | import (
4 | "errors"
5 | "strings"
6 |
7 | oas3 "github.com/getkin/kin-openapi/openapi3"
8 | "github.com/grokify/mogo/encoding/jsonpointer"
9 | "github.com/grokify/spectrum/openapi3"
10 | "github.com/grokify/spectrum/openapi3lint/lintutil"
11 | )
12 |
13 | type RuleOperationXPropertyStringExist struct {
14 | name string
15 | xPropertyName string
16 | inclSpec bool
17 | inclPathItem bool
18 | }
19 |
20 | func NewRule(ruleName, xPropertyName string, inclSpec, inclPathItem bool) (RuleOperationXPropertyStringExist, error) {
21 | ruleName = strings.ToLower(strings.TrimSpace(ruleName))
22 | xPropertyName = strings.TrimSpace(xPropertyName)
23 |
24 | if len(ruleName) == 0 {
25 | return RuleOperationXPropertyStringExist{},
26 | errors.New("rule name not provided")
27 | }
28 | if len(xPropertyName) == 0 {
29 | return RuleOperationXPropertyStringExist{},
30 | errors.New("x-property name not provided")
31 | }
32 |
33 | rule := RuleOperationXPropertyStringExist{
34 | name: ruleName,
35 | xPropertyName: xPropertyName,
36 | inclSpec: inclSpec,
37 | inclPathItem: inclPathItem}
38 | return rule, nil
39 | }
40 |
41 | func (rule RuleOperationXPropertyStringExist) Name() string {
42 | return rule.name
43 | }
44 |
45 | func (rule RuleOperationXPropertyStringExist) Scope() string {
46 | return lintutil.ScopeSpecification
47 | }
48 |
49 | func (rule RuleOperationXPropertyStringExist) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
50 | return []lintutil.PolicyViolation{}
51 | }
52 |
53 | func (rule RuleOperationXPropertyStringExist) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
54 | vios := []lintutil.PolicyViolation{}
55 | if spec == nil {
56 | return vios
57 | }
58 | if rule.inclSpec {
59 | propVal := strings.TrimSpace(openapi3.GetExtensionPropStringOrEmpty(
60 | spec.Extensions, rule.xPropertyName))
61 | // spec.ExtensionProps, rule.xPropertyName))
62 | if len(propVal) > 0 {
63 | return vios
64 | }
65 | }
66 | pathsMap := spec.Paths.Map()
67 | for pathURL, pathItem := range pathsMap {
68 | // for pathURL, pathItem := range spec.Paths { // getkin v0.121.0 to v0.122.0
69 | if pathItem == nil {
70 | continue
71 | }
72 | if rule.inclPathItem {
73 | propVal := strings.TrimSpace(openapi3.GetExtensionPropStringOrEmpty(
74 | pathItem.Extensions, rule.xPropertyName))
75 | // pathItem.ExtensionProps, rule.xPropertyName))
76 | if len(propVal) > 0 {
77 | continue
78 | }
79 | }
80 | openapi3.VisitOperationsPathItem(pathURL, pathItem,
81 | func(path, method string, op *oas3.Operation) {
82 | if op == nil {
83 | return
84 | }
85 | propVal := strings.TrimSpace(openapi3.GetExtensionPropStringOrEmpty(
86 | op.Extensions, rule.xPropertyName))
87 | // op.ExtensionProps, rule.xPropertyName))
88 | if len(propVal) == 0 {
89 | vios = append(vios, lintutil.PolicyViolation{
90 | RuleName: rule.Name(),
91 | Location: jsonpointer.PointerSubEscapeAll(
92 | "%s#/paths/%s/%s/%s",
93 | pointerBase, pathURL, method, rule.xPropertyName,
94 | ),
95 | })
96 | }
97 | },
98 | )
99 | }
100 | return vios
101 | }
102 |
--------------------------------------------------------------------------------
/openapi3edit/delete.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import (
4 | "net/http"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/net/urlutil"
8 | "github.com/grokify/spectrum/openapi3"
9 | )
10 |
11 | func (se *SpecEdit) DeleteProperties(md openapi3.SpecMetadata) {
12 | if se.SpecMore.Spec == nil {
13 | return
14 | }
15 | spec := se.SpecMore.Spec
16 | for _, opID := range md.OperationIDs {
17 | se.DeleteOperations(
18 | func(urlpath, method string, op *oas3.Operation) bool {
19 | if op != nil && op.OperationID == opID {
20 | return true
21 | }
22 | return false
23 | })
24 | }
25 | for _, epDel := range md.Endpoints {
26 | se.DeleteOperations(
27 | func(urlpath, method string, op *oas3.Operation) bool {
28 | if op == nil {
29 | return false
30 | }
31 | if epDel == urlutil.EndpointString(urlpath, method, false) ||
32 | epDel == urlutil.EndpointString(urlpath, method, true) {
33 | return true
34 | }
35 | return false
36 | })
37 | }
38 | for _, schemaNameDel := range md.SchemaNames {
39 | for schemaNameTry := range spec.Components.Schemas {
40 | if schemaNameDel == schemaNameTry {
41 | delete(spec.Components.Schemas, schemaNameTry)
42 | }
43 | }
44 | }
45 | }
46 |
47 | func (se *SpecEdit) DeleteOperations(delThis func(urlpath, method string, op *oas3.Operation) bool) {
48 | if se.SpecMore.Spec == nil {
49 | return
50 | }
51 | // newPaths := oas3.Paths{} // getkin v0.121.0 to v0.122.0
52 | newPaths := oas3.NewPaths()
53 |
54 | pathsMap := se.SpecMore.Spec.Paths.Map()
55 | for urlpath, pathItem := range pathsMap {
56 | // for urlpath, pathItem := range se.SpecMore.Spec.Paths { // getkin v0.121.0 to v0.122.0
57 | newPathItem := oas3.PathItem{
58 | // ExtensionProps: pathItem.ExtensionProps,
59 | Extensions: pathItem.Extensions,
60 | Ref: pathItem.Ref,
61 | Summary: pathItem.Summary,
62 | Description: pathItem.Description,
63 | Servers: pathItem.Servers,
64 | Parameters: pathItem.Parameters}
65 | if pathItem.Connect != nil && !delThis(urlpath, http.MethodConnect, pathItem.Connect) {
66 | newPathItem.Connect = pathItem.Connect
67 | }
68 | if pathItem.Delete != nil && !delThis(urlpath, http.MethodDelete, pathItem.Delete) {
69 | newPathItem.Delete = pathItem.Delete
70 | }
71 | if pathItem.Get != nil && !delThis(urlpath, http.MethodGet, pathItem.Get) {
72 | newPathItem.Get = pathItem.Get
73 | }
74 | if pathItem.Head != nil && !delThis(urlpath, http.MethodHead, pathItem.Head) {
75 | newPathItem.Head = pathItem.Head
76 | }
77 | if pathItem.Options != nil && !delThis(urlpath, http.MethodOptions, pathItem.Options) {
78 | newPathItem.Options = pathItem.Options
79 | }
80 | if pathItem.Patch != nil && !delThis(urlpath, http.MethodPatch, pathItem.Patch) {
81 | newPathItem.Patch = pathItem.Patch
82 | }
83 | if pathItem.Post != nil && !delThis(urlpath, http.MethodPost, pathItem.Post) {
84 | newPathItem.Post = pathItem.Post
85 | }
86 | if pathItem.Put != nil && !delThis(urlpath, http.MethodPut, pathItem.Put) {
87 | newPathItem.Put = pathItem.Put
88 | }
89 | if pathItem.Trace != nil && !delThis(urlpath, http.MethodTrace, pathItem.Trace) {
90 | newPathItem.Trace = pathItem.Trace
91 | }
92 | if openapi3.PathItemHasEndpoints(&newPathItem) {
93 | newPaths.Set(urlpath, &newPathItem)
94 | // newPaths[urlpath] = &newPathItem // getkin v0.121.0 to v0.122.0
95 | }
96 | }
97 | se.SpecMore.Spec.Paths = newPaths
98 | }
99 |
--------------------------------------------------------------------------------
/openapi3lint/ruleschemaobjectpropsexist/rule_schema_object_props_exist.go:
--------------------------------------------------------------------------------
1 | // ruleschemaobjectpropsexist ensures that schema objects have properties.
2 | package ruleschemaobjectpropsexist
3 |
4 | import (
5 | oas3 "github.com/getkin/kin-openapi/openapi3"
6 | "github.com/grokify/mogo/encoding/jsonpointer"
7 | "github.com/grokify/spectrum/openapi3"
8 | "github.com/grokify/spectrum/openapi3lint/lintutil"
9 | )
10 |
11 | type RuleSchemaObjectPropsExist struct {
12 | name string
13 | }
14 |
15 | func NewRule() RuleSchemaObjectPropsExist {
16 | return RuleSchemaObjectPropsExist{
17 | name: lintutil.RulenameSchemaObjectPropsExist}
18 | }
19 |
20 | func (rule RuleSchemaObjectPropsExist) Name() string {
21 | return rule.name
22 | }
23 |
24 | func (rule RuleSchemaObjectPropsExist) Scope() string {
25 | return lintutil.ScopeSpecification
26 | }
27 |
28 | func (rule RuleSchemaObjectPropsExist) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
29 | return nil
30 | }
31 |
32 | func (rule RuleSchemaObjectPropsExist) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
33 | vios := []lintutil.PolicyViolation{}
34 |
35 | for schName, schRef := range spec.Components.Schemas {
36 | if schRef == nil || schRef.Value == nil || !openapi3.TypesRefIs(schRef.Value.Type, openapi3.TypeObject) {
37 | continue
38 | }
39 | /*
40 | if len(schRef.Value.Properties) == 0 && schRef.Value.AdditionalProperties == nil &&
41 | (schRef.Value.AdditionalPropertiesAllowed == nil || !*schRef.Value.AdditionalPropertiesAllowed) {
42 | vios = append(vios, lintutil.PolicyViolation{
43 | RuleName: rule.Name(),
44 | Location: jsonpointer.PointerSubEscapeAll(
45 | "%s#/components/schemas/%s",
46 | pointerBase, schName)})
47 | }
48 | */
49 | if len(schRef.Value.Properties) == 0 && !openapi3.AdditionalPropertiesExists(schRef.Value.AdditionalProperties) {
50 | // (!openapi3.AdditionalPropertiesAllowed(schRef.Value.AdditionalProperties) ||
51 | // len(schRef.Value.AdditionalProperties) == 0) {
52 | vios = append(vios, lintutil.PolicyViolation{
53 | RuleName: rule.Name(),
54 | Location: jsonpointer.PointerSubEscapeAll(
55 | "%s#/components/schemas/%s",
56 | pointerBase, schName)})
57 | }
58 | for propName, propRef := range schRef.Value.Properties {
59 | if propRef == nil || propRef.Value == nil || !openapi3.TypesRefIs(propRef.Value.Type, openapi3.TypeObject) {
60 | continue
61 | }
62 | /*
63 | if len(propRef.Value.Properties) == 0 &&
64 | propRef.Value.AdditionalProperties == nil &&
65 | (propRef.Value.AdditionalPropertiesAllowed == nil || !*propRef.Value.AdditionalPropertiesAllowed) {
66 | vios = append(vios, lintutil.PolicyViolation{
67 | RuleName: rule.Name(),
68 | Location: jsonpointer.PointerSubEscapeAll(
69 | "%s#/components/schemas/%s/properties/%s",
70 | pointerBase, schName, propName)})
71 | }
72 | */
73 | if len(propRef.Value.Properties) == 0 && !openapi3.AdditionalPropertiesExists(propRef.Value.AdditionalProperties) {
74 | // propRef.Value.AdditionalProperties == nil &&
75 | // (propRef.Value.AdditionalPropertiesAllowed == nil || !*propRef.Value.AdditionalPropertiesAllowed) {
76 | vios = append(vios, lintutil.PolicyViolation{
77 | RuleName: rule.Name(),
78 | Location: jsonpointer.PointerSubEscapeAll(
79 | "%s#/components/schemas/%s/properties/%s",
80 | pointerBase, schName, propName)})
81 | }
82 | }
83 | }
84 | return vios
85 | }
86 |
--------------------------------------------------------------------------------
/cmd/oas3lint/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 |
7 | "github.com/grokify/mogo/fmt/fmtutil"
8 | "github.com/grokify/mogo/log/logutil"
9 | "github.com/grokify/mogo/os/osutil"
10 | "github.com/grokify/spectrum/openapi3lint"
11 | "github.com/grokify/spectrum/openapi3lint/lintutil"
12 | flags "github.com/jessevdk/go-flags"
13 | )
14 |
15 | type Options struct {
16 | PolicyFile string `short:"p" long:"policyfile" description:"Policy File" required:"true"`
17 | InputFileOAS3 string `short:"i" long:"inputspec" description:"Input OAS Spec File or Dir" required:"false"`
18 | Severity string `short:"s" long:"severity" description:"Severity level"`
19 | }
20 |
21 | func main() {
22 | var opts Options
23 | _, err := flags.Parse(&opts)
24 | logutil.FatalErr(err)
25 | fmtutil.MustPrintJSON(opts)
26 |
27 | vsets, err := ValidateSpecFiles(opts.InputFileOAS3, opts.PolicyFile, opts.Severity)
28 | logutil.FatalErr(err)
29 |
30 | fmtutil.MustPrintJSON(vsets.LocationsByRule())
31 | fmtutil.MustPrintJSON(vsets.CountsByRule())
32 |
33 | fmt.Println("DONE")
34 | }
35 |
36 | func ValidateSpecFiles(specFileOrDir string, policyfile, sev string) (*lintutil.PolicyViolationsSets, error) {
37 | files, err := filesFromFileOrDir(specFileOrDir)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | polCfg, err := openapi3lint.NewPolicyConfigFile(policyfile)
43 | if err != nil {
44 | return nil, err
45 | }
46 | //polCfg.AddRuleCollection(extensions.NewRuleCollectionExtensions())
47 | //logutil.FatalErr(fmtutil.PrintJSON(polCfg))
48 | //logutil.FatalErr(fmtutil.PrintJSON(polCfg.RuleNames()))
49 |
50 | pol, err := polCfg.Policy()
51 | if err != nil {
52 | return nil, err
53 | }
54 | fmtutil.MustPrintJSON(pol)
55 | fmtutil.MustPrintJSON(pol.RuleNames())
56 |
57 | return pol.ValidateSpecFiles(sev, files)
58 | }
59 |
60 | func filesFromFileOrDir(filename string) ([]string, error) {
61 | return osutil.Filenames(filename, regexp.MustCompile(`(?i)\.(json|yaml|yml)$`), false, false)
62 | }
63 |
64 | /*
65 | func filesFromFileOrDirOld(filename string) ([]string, error) {
66 | var files []string
67 | if len(filename) > 0 {
68 | isDir, err := osutil.IsDir(filename)
69 | if err != nil {
70 | return files, err
71 | }
72 |
73 | if isDir {
74 | entries, err := osutil.ReadDirMore(filename,
75 | regexp.MustCompile(`(?i)\.(json|yaml|yml)$`), false, true, false)
76 | logutil.FatalErr(err)
77 |
78 | files = osutil.DirEntries(entries).Names(filename, true)
79 | } else {
80 | files = []string{filename}
81 | }
82 | } else {
83 | files = []string{filename}
84 | }
85 | return files, nil
86 | }
87 | */
88 |
89 | /*
90 | func getPolicyConfig() openapi3lint.PolicyConfig {
91 | return openapi3lint.PolicyConfig{
92 | Rules: map[string]openapi3lint.RuleConfig{
93 | openapi3lint.RuleOpIdStyleCamelCase: {
94 | Severity: severity.SeverityError},
95 | openapi3lint.RuleOpSummaryExist: {
96 | Severity: severity.SeverityError},
97 | openapi3lint.RuleOpSummaryCaseFirstCapitalized: {
98 | Severity: severity.SeverityError},
99 | openapi3lint.RulePathParamStyleCamelCase: {
100 | Severity: severity.SeverityError},
101 | openapi3lint.RuleSchemaObjectPropsExist: {
102 | Severity: severity.SeverityError},
103 | openapi3lint.RuleSchemaPropEnumStylePascalCase: {
104 | Severity: severity.SeverityError},
105 | openapi3lint.RuleTagCaseFirstCapitalized: {
106 | Severity: severity.SeverityError},
107 | },
108 | }
109 | }
110 | */
111 |
--------------------------------------------------------------------------------
/openapi3edit/multi.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import (
4 | "fmt"
5 |
6 | oas3 "github.com/getkin/kin-openapi/openapi3"
7 | "github.com/grokify/mogo/errors/errorsutil"
8 | "github.com/grokify/mogo/fmt/fmtutil"
9 | "github.com/grokify/spectrum/openapi3"
10 | )
11 |
12 | type SpecMoreModifyMultiOpts struct {
13 | OperationsDeleteFunc func(opPath, opMethod string, op *oas3.Operation) bool
14 | OperationsRenameIDsFunc func(string, string, *oas3.Operation)
15 | OperationsRemoveSecurity bool
16 | OperationsShowIDs bool
17 | OperationsExec bool
18 | Paths SpecPathsModifyOpts
19 | PathsShow bool
20 | PathsExec bool
21 | TagsOperationFunc func(string, string, *oas3.Operation)
22 | Tags map[string]string
23 | TagsShow bool
24 | TagsExec bool
25 | }
26 |
27 | // SpecMoreModifyMulti is used to perform multiple updates on an OpenAPI 3 spec.
28 | func SpecMoreModifyMulti(sm *openapi3.SpecMore, opts SpecMoreModifyMultiOpts) error {
29 | se := SpecEdit{SpecMore: *sm}
30 | if opts.OperationsShowIDs {
31 | // fmtutil.PrintJSON(SpecOperationIds(sm.Spec))
32 | oldIDs := sm.OperationIDsCounts()
33 | if opts.OperationsShowIDs {
34 | err := fmtutil.PrintJSON(oldIDs)
35 | if err != nil {
36 | return err
37 | }
38 | }
39 | for id, count := range oldIDs {
40 | if count != 1 {
41 | return fmt.Errorf("E_OPERATION_ID_BAD_COUNT ID[%s]COUNT[%d]", id, count)
42 | }
43 | }
44 | }
45 | if opts.OperationsExec {
46 | if opts.OperationsDeleteFunc != nil {
47 | se.DeleteOperations(opts.OperationsDeleteFunc)
48 | }
49 | if opts.OperationsRenameIDsFunc != nil {
50 | openapi3.VisitOperations(sm.Spec, opts.OperationsRenameIDsFunc)
51 | }
52 | // UpdateOperationIds(sm.Spec, opts.OperationIdsRename)
53 | newIDs := sm.OperationIDsCounts()
54 | if opts.OperationsShowIDs {
55 | err := fmtutil.PrintJSON(newIDs)
56 | if err != nil {
57 | return err
58 | }
59 | }
60 | for id, count := range newIDs {
61 | if count != 1 {
62 | return fmt.Errorf("E_OPERATION_ID_BAD_COUNT_AFTER_RENAME ID[%s]COUNT[%d]", id, count)
63 | }
64 | }
65 |
66 | if opts.OperationsRemoveSecurity {
67 | err := se.OperationsSecurityRemove([]string{})
68 | if err != nil {
69 | return err
70 | }
71 | }
72 | }
73 | // Update Paths
74 | if opts.PathsShow {
75 | err := fmtutil.PrintJSON(InspectPaths(sm.Spec))
76 | if err != nil {
77 | return err
78 | }
79 | }
80 | if opts.PathsExec {
81 | err := se.PathsModify(opts.Paths)
82 | if err != nil {
83 | return errorsutil.Wrap(err, "specModifyMulti")
84 | }
85 | if opts.PathsShow {
86 | err := fmtutil.PrintJSON(InspectPaths(sm.Spec))
87 | if err != nil {
88 | return err
89 | }
90 | }
91 | }
92 |
93 | // Update Tags
94 | if opts.TagsOperationFunc != nil || len(opts.Tags) > 0 {
95 | if opts.TagsShow {
96 | err := fmtutil.PrintJSON(sm.TagsMap(&openapi3.TagsOpts{InclDefs: true, InclOps: true}))
97 | if err != nil {
98 | return err
99 | }
100 | }
101 | if opts.TagsExec {
102 | if opts.TagsOperationFunc != nil {
103 | openapi3.VisitOperations(sm.Spec, opts.TagsOperationFunc)
104 | }
105 | se := SpecEdit{SpecMore: *sm}
106 | se.TagsModify(opts.Tags)
107 | if opts.TagsShow {
108 | err := fmtutil.PrintJSON(sm.TagsMap(&openapi3.TagsOpts{InclDefs: true, InclOps: true}))
109 | if err != nil {
110 | return err
111 | }
112 | }
113 | }
114 | }
115 |
116 | return nil
117 | }
118 |
--------------------------------------------------------------------------------
/openapi3/visit.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 |
8 | oas3 "github.com/getkin/kin-openapi/openapi3"
9 | "github.com/grokify/mogo/encoding/jsonpointer"
10 | )
11 |
12 | const (
13 | jPtrParamFormat = "#/components/parameters/%s"
14 | // jPtrSchemasRoot = "#/components/schemas/"
15 | jPtrSchemaPropertyFormat = "#/components/schemas/%s/properties/%s"
16 | )
17 |
18 | func VisitTypesFormats(spec *Spec, visitTypeFormat func(jsonPointerRoot, oasType, oasFormat string)) {
19 | for schemaName, schemaRef := range spec.Components.Schemas {
20 | if schemaRef.Value == nil {
21 | continue
22 | }
23 | for propName, propRef := range schemaRef.Value.Properties {
24 | if propRef.Value == nil || propRef.Value.Type == nil {
25 | continue
26 | }
27 | for _, t := range *propRef.Value.Type {
28 | visitTypeFormat(
29 | fmt.Sprintf(jPtrSchemaPropertyFormat, schemaName, propName),
30 | t,
31 | propRef.Value.Format)
32 | }
33 | }
34 | }
35 | for paramName, paramRef := range spec.Components.Parameters {
36 | if paramRef.Value == nil ||
37 | paramRef.Value.Schema == nil ||
38 | paramRef.Value.Schema.Value == nil ||
39 | paramRef.Value.Schema.Value.Type == nil {
40 | continue
41 | }
42 | for _, t := range *paramRef.Value.Schema.Value.Type {
43 | visitTypeFormat(
44 | fmt.Sprintf(jPtrParamFormat, paramName),
45 | t,
46 | paramRef.Value.Schema.Value.Format)
47 | }
48 | }
49 | VisitOperations(
50 | spec,
51 | func(path, method string, op *oas3.Operation) {
52 | if op == nil {
53 | return
54 | }
55 | for i, paramRef := range op.Parameters {
56 | if paramRef.Value == nil ||
57 | paramRef.Value.Schema == nil ||
58 | paramRef.Value.Schema.Value == nil ||
59 | paramRef.Value.Schema.Value.Type == nil {
60 | continue
61 | }
62 | for _, t := range *paramRef.Value.Schema.Value.Type {
63 | visitTypeFormat(
64 | jsonpointer.PointerSubEscapeAll(
65 | "#/paths/%s/%s/parameters/%d/schema", path, strings.ToLower(method), i),
66 | t,
67 | paramRef.Value.Schema.Value.Format)
68 | }
69 | }
70 | },
71 | )
72 | }
73 |
74 | func VisitOperationsPathItem(path string, pathItem *oas3.PathItem, visitOp func(path, method string, op *oas3.Operation)) {
75 | pathURL := path
76 | if pathItem == nil {
77 | return
78 | }
79 | if pathItem.Connect != nil {
80 | visitOp(pathURL, http.MethodConnect, pathItem.Connect)
81 | }
82 | if pathItem.Delete != nil {
83 | visitOp(pathURL, http.MethodDelete, pathItem.Delete)
84 | }
85 | if pathItem.Get != nil {
86 | visitOp(pathURL, http.MethodGet, pathItem.Get)
87 | }
88 | if pathItem.Head != nil {
89 | visitOp(pathURL, http.MethodHead, pathItem.Head)
90 | }
91 | if pathItem.Options != nil {
92 | visitOp(pathURL, http.MethodOptions, pathItem.Options)
93 | }
94 | if pathItem.Patch != nil {
95 | visitOp(pathURL, http.MethodPatch, pathItem.Patch)
96 | }
97 | if pathItem.Post != nil {
98 | visitOp(pathURL, http.MethodPost, pathItem.Post)
99 | }
100 | if pathItem.Put != nil {
101 | visitOp(pathURL, http.MethodPut, pathItem.Put)
102 | }
103 | if pathItem.Trace != nil {
104 | visitOp(pathURL, http.MethodTrace, pathItem.Trace)
105 | }
106 | }
107 |
108 | func VisitOperations(spec *Spec, visitOp func(path, method string, op *oas3.Operation)) {
109 | pathsMap := spec.Paths.Map()
110 | for path, pathItem := range pathsMap {
111 | // for path, pathItem := range spec.Paths { // getkin v0.121.0 to v0.122.0
112 | VisitOperationsPathItem(path, pathItem, visitOp)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/openapi2/count.go:
--------------------------------------------------------------------------------
1 | package openapi2
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/grokify/gocharts/v2/data/histogram"
10 | "github.com/grokify/mogo/encoding/csvutil"
11 | "github.com/grokify/mogo/type/stringsutil"
12 | )
13 |
14 | func CountEndpointsByTag(spec Specification, tagsFilter []string) *histogram.HistogramSet {
15 | tagsFilter = stringsutil.SliceCondenseSpace(tagsFilter, true, true)
16 | hist := histogram.NewHistogramSet("endpoints by tag")
17 | for url, path := range spec.Paths {
18 | hist = countEndpointByTag(hist, tagsFilter, url, http.MethodGet, path.Get)
19 | hist = countEndpointByTag(hist, tagsFilter, url, http.MethodPatch, path.Patch)
20 | hist = countEndpointByTag(hist, tagsFilter, url, http.MethodPut, path.Put)
21 | hist = countEndpointByTag(hist, tagsFilter, url, http.MethodPost, path.Post)
22 | hist = countEndpointByTag(hist, tagsFilter, url, http.MethodDelete, path.Delete)
23 | }
24 | return hist
25 | }
26 |
27 | func countEndpointByTag(hist *histogram.HistogramSet, tagsFilter []string, url string, method string, ep *Endpoint) *histogram.HistogramSet {
28 | if ep == nil {
29 | return hist
30 | }
31 | method = strings.ToUpper(strings.TrimSpace(method))
32 | url = strings.TrimSpace(url)
33 | endpoint := method + " " + url
34 | for _, tag := range ep.Tags {
35 | tag = strings.TrimSpace(tag)
36 | add := true
37 | if len(tagsFilter) > 0 { // have tagsFilter
38 | add = false
39 | for _, try := range tagsFilter {
40 | if tag == try {
41 | add = true
42 | }
43 | }
44 | }
45 | if !add {
46 | continue
47 | }
48 | if len(tag) > 0 {
49 | hist.Add(tag, endpoint, 1)
50 | }
51 | }
52 | return hist
53 | }
54 |
55 | func WriteEndpointCountCSV(filename string, hset histogram.HistogramSet) error {
56 | writer, file, err := csvutil.NewWriterFile(filename)
57 | if err != nil {
58 | return err
59 | }
60 | //defer file.Close()
61 | //defer writer.Close()
62 | header := []string{"Tag", "Tag Endpoint Count", "Method", "Path"}
63 | err = writer.Write(header)
64 | if err != nil {
65 | return err
66 | }
67 | for tagName, hist := range hset.Items {
68 | hist.Inflate()
69 | for endpoint := range hist.Items {
70 | parts := strings.Split(endpoint, " ")
71 | if len(parts) >= 2 {
72 | row := []string{
73 | tagName,
74 | strconv.Itoa(len(hist.Items)),
75 | strings.ToUpper(parts[0]),
76 | strings.Join(parts[1:], " ")}
77 | err := writer.Write(row)
78 | if err != nil {
79 | return err
80 | }
81 | }
82 | }
83 | }
84 | writer.Flush()
85 | if err = writer.Error(); err != nil {
86 | return err
87 | } else {
88 | return file.Close()
89 | }
90 | }
91 |
92 | // EndpointCount returns a count of the endpoints for a specification.
93 | func EndpointCount(spec Specification) int {
94 | endpoints := map[string]int{}
95 | for url, path := range spec.Paths {
96 | url = strings.TrimSpace(url)
97 | if path.Get != nil && !path.Get.IsEmpty() {
98 | endpoints[fmt.Sprintf("%s %s", http.MethodGet, url)] = 1
99 | }
100 | if path.Patch != nil && !path.Patch.IsEmpty() {
101 | endpoints[fmt.Sprintf("%s %s", http.MethodPatch, url)] = 1
102 | }
103 | if path.Post != nil && !path.Post.IsEmpty() {
104 | endpoints[fmt.Sprintf("%s %s", http.MethodPost, url)] = 1
105 | }
106 | if path.Put != nil && !path.Put.IsEmpty() {
107 | endpoints[fmt.Sprintf("%s %s", http.MethodPut, url)] = 1
108 | }
109 | if path.Delete != nil && !path.Delete.IsEmpty() {
110 | endpoints[fmt.Sprintf("%s %s", http.MethodDelete, url)] = 1
111 | }
112 | }
113 | return len(endpoints)
114 | }
115 |
--------------------------------------------------------------------------------
/openapi3/openapi3html/openapi3html/page.qtpl:
--------------------------------------------------------------------------------
1 | {% func SpectrumUIPage(data PageParams) %}
2 |
3 |
4 |
5 | {%s data.PageTitle %}
6 |
7 |
8 |
9 |
10 |
11 | {%s= data.PageLinkHTML() %}
12 |
13 |
14 |
15 |
110 |
111 |
112 |
113 | {% endfunc %}
--------------------------------------------------------------------------------
/openapi3lint/rulepathparamstyle/rule_path_param_style.go:
--------------------------------------------------------------------------------
1 | package rulepathparamstyle
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strconv"
7 |
8 | oas3 "github.com/getkin/kin-openapi/openapi3"
9 | "github.com/grokify/mogo/encoding/jsonpointer"
10 | "github.com/grokify/mogo/text/stringcase"
11 | "github.com/grokify/spectrum/openapi3"
12 | "github.com/grokify/spectrum/openapi3lint/lintutil"
13 | )
14 |
15 | type RulePathParamStyle struct {
16 | name string
17 | stringCase string
18 | }
19 |
20 | func NewRule(requiredStringCase string) (RulePathParamStyle, error) {
21 | canonicalCase, err := stringcase.Parse(requiredStringCase)
22 | if err != nil {
23 | return RulePathParamStyle{},
24 | fmt.Errorf("invalid string case [%s]", requiredStringCase)
25 | }
26 | rule := RulePathParamStyle{
27 | stringCase: canonicalCase}
28 | switch canonicalCase {
29 | case stringcase.CamelCase:
30 | rule.name = lintutil.RulenamePathParamStyleCamelCase
31 | case stringcase.KebabCase:
32 | rule.name = lintutil.RulenamePathParamStyleKebabCase
33 | case stringcase.PascalCase:
34 | rule.name = lintutil.RulenamePathParamStylePascalCase
35 | case stringcase.SnakeCase:
36 | rule.name = lintutil.RulenamePathParamStyleSnakeCase
37 | default:
38 | return rule, fmt.Errorf("invalid string case [%s]", canonicalCase)
39 | }
40 | return rule, nil
41 | }
42 |
43 | func (rule RulePathParamStyle) Name() string {
44 | return rule.name
45 | }
46 |
47 | func (rule RulePathParamStyle) Scope() string {
48 | return lintutil.ScopeOperation
49 | }
50 |
51 | func (rule RulePathParamStyle) ProcessOperation(spec *openapi3.Spec, op *oas3.Operation, opPointer, path, method string) []lintutil.PolicyViolation {
52 | return nil
53 | }
54 |
55 | var rxParams = regexp.MustCompile(`{([^/]*)}`)
56 |
57 | func (rule RulePathParamStyle) ProcessSpec(spec *openapi3.Spec, pointerBase string) []lintutil.PolicyViolation {
58 | vios := []lintutil.PolicyViolation{}
59 |
60 | pathsMap := spec.Paths.Map()
61 | for pathURL := range pathsMap {
62 | // for pathURL := range spec.Paths { // getkin v0.121.0 to v0.122.0
63 | m := rxParams.FindAllStringSubmatch(pathURL, -1)
64 | if len(m) == 0 {
65 | continue
66 | }
67 | jsPtr := jsonpointer.PointerSubEscapeAll("%s#/paths/%s",
68 | pointerBase, pathURL)
69 | for _, mi := range m {
70 | isWantCase, err := stringcase.IsCase(rule.stringCase, mi[1])
71 | if err != nil {
72 | vios = append(vios, lintutil.PolicyViolation{
73 | RuleName: rule.Name(),
74 | Location: jsPtr,
75 | Value: err.Error()})
76 | } else if !isWantCase {
77 | vios = append(vios, lintutil.PolicyViolation{
78 | RuleName: rule.Name(),
79 | Location: jsPtr})
80 | }
81 | }
82 | }
83 |
84 | openapi3.VisitOperations(spec, func(path, method string, op *oas3.Operation) {
85 | if op == nil {
86 | return
87 | }
88 | jsPtr := jsonpointer.PointerSubEscapeAll(
89 | "%s#/paths/%s/%s/parameters/",
90 | pointerBase,
91 | path,
92 | method)
93 | for i, paramRef := range op.Parameters {
94 | if paramRef == nil || paramRef.Value == nil {
95 | continue
96 | }
97 | isWantCase, err := stringcase.IsCase(rule.stringCase, paramRef.Value.Name)
98 | if err != nil {
99 | // error should not occur as stringCase should be pre-validated.
100 | vios = append(vios, lintutil.PolicyViolation{
101 | RuleName: rule.Name(),
102 | Location: jsPtr + strconv.Itoa(i),
103 | Value: paramRef.Value.Name + " " + err.Error()})
104 | } else if !isWantCase {
105 | vios = append(vios, lintutil.PolicyViolation{
106 | RuleName: rule.Name(),
107 | Location: jsPtr + strconv.Itoa(i),
108 | Value: paramRef.Value.Name})
109 | }
110 | }
111 | })
112 |
113 | return vios
114 | }
115 |
--------------------------------------------------------------------------------
/openapi3/parameter.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "net/url"
5 | "regexp"
6 | "strings"
7 |
8 | oas3 "github.com/getkin/kin-openapi/openapi3"
9 | "github.com/grokify/mogo/encoding/jsonpointer"
10 | "github.com/grokify/mogo/type/maputil"
11 | "github.com/grokify/mogo/type/stringsutil"
12 | )
13 |
14 | // ParameterNames covers path template parameters, operation parameter names/referecnes, compoents keys/names
15 | // Parameter path names only. This is useful for viewing and modifying parameter template naems.
16 | type ParameterNames struct {
17 | Components maputil.MapStringSlice
18 | Operations maputil.MapStringSlice
19 | Paths maputil.MapStringSlice
20 | }
21 |
22 | func (pn *ParameterNames) Names() []string {
23 | names := map[string]int{}
24 | for name := range pn.Components {
25 | names[name]++
26 | }
27 | for name := range pn.Operations {
28 | names[name]++
29 | }
30 | for name := range pn.Paths {
31 | names[name]++
32 | }
33 | return maputil.StringKeys(names, nil)
34 | }
35 |
36 | func NewParameterNames() ParameterNames {
37 | return ParameterNames{
38 | Components: maputil.MapStringSlice{},
39 | Operations: maputil.MapStringSlice{},
40 | Paths: maputil.MapStringSlice{},
41 | }
42 | }
43 |
44 | // ParameterPathNames returns a set of parameter names. Parameter names exist in (1) path URLs,
45 | // (2) operation parameters and (3) spec component parameters.
46 | func (sm *SpecMore) ParameterPathNames() ParameterNames {
47 | if sm.Spec == nil {
48 | return NewParameterNames()
49 | }
50 | return ParameterNames{
51 | Components: sm.ParamPathNamesComponents(),
52 | Operations: sm.ParamPathNamesOperations(),
53 | Paths: sm.ParamPathNamesPaths()}
54 | }
55 |
56 | func (sm *SpecMore) ParamPathNamesComponents() map[string][]string {
57 | names := url.Values{}
58 | if sm.Spec == nil {
59 | return names
60 | }
61 | for paramKey, paramRef := range sm.Spec.Components.Parameters {
62 | if paramRef.Value == nil {
63 | continue
64 | }
65 | if strings.ToLower(strings.TrimSpace(paramRef.Value.In)) != InPath {
66 | continue
67 | }
68 | if len(paramRef.Value.Name) > 0 {
69 | jpath := jsonpointer.PointerSubEscapeAll(`#/components/parameters/%s/name`, paramKey)
70 | names.Add(paramRef.Value.Name, jpath)
71 | }
72 | }
73 | return names
74 | }
75 |
76 | func (sm *SpecMore) ParamPathNamesOperations() map[string][]string {
77 | names := url.Values{}
78 | if sm.Spec == nil {
79 | return names
80 | }
81 | VisitOperations(sm.Spec, func(opPath, opMethod string, op *oas3.Operation) {
82 | if op == nil {
83 | return
84 | }
85 | for i, paramRef := range op.Parameters {
86 | if paramRef.Value == nil {
87 | continue
88 | }
89 | if strings.ToLower(strings.TrimSpace(paramRef.Value.In)) != InPath {
90 | continue
91 | }
92 | jpath := jsonpointer.PointerSubEscapeAll(`#/paths/%s/%s/parameters/%d/name`, opPath, opMethod, i)
93 | names.Add(paramRef.Value.Name, jpath)
94 | }
95 | })
96 | return names
97 | }
98 |
99 | func (sm *SpecMore) ParamPathNamesPaths() map[string][]string {
100 | names := url.Values{}
101 | if sm.Spec == nil {
102 | return names
103 | }
104 | pathsMap := sm.Spec.Paths.Map()
105 | for pathURL := range pathsMap {
106 | // for pathURL := range sm.Spec.Paths { // getkin v0.121.0 to v0.122.0
107 | m := PathParams(pathURL)
108 | if len(m) > 0 {
109 | jpath := jsonpointer.PointerSubEscapeAll(`#/paths/%s`, pathURL)
110 | for _, paramName := range m {
111 | names.Add(paramName, jpath)
112 | }
113 | }
114 | }
115 | return names
116 | }
117 |
118 | var rxParens = regexp.MustCompile(`{([^}{}]+)}`)
119 |
120 | func ParsePathParametersParens(urlPath string) []string {
121 | paramNames := []string{}
122 | m := rxParens.FindAllStringSubmatch(urlPath, -1)
123 | if len(m) == 0 {
124 | return paramNames
125 | }
126 | for _, n := range m {
127 | if len(n) == 2 {
128 | varName := strings.TrimSpace(n[1])
129 | paramNames = append(paramNames, varName)
130 | }
131 | }
132 | if len(paramNames) > 0 {
133 | paramNames = stringsutil.SliceCondenseSpace(paramNames, true, false)
134 | }
135 | return paramNames
136 | }
137 |
--------------------------------------------------------------------------------
/openapi3/read.go:
--------------------------------------------------------------------------------
1 | package openapi3
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "os"
8 | "regexp"
9 | "strings"
10 |
11 | oas3 "github.com/getkin/kin-openapi/openapi3"
12 | "github.com/grokify/mogo/encoding/jsonutil"
13 | "github.com/grokify/mogo/errors/errorsutil"
14 | "sigs.k8s.io/yaml"
15 | )
16 |
17 | var rxYamlExtension = regexp.MustCompile(`(?i)\.ya?ml\s*$`)
18 |
19 | func ReadURL(oas3url string) (*Spec, error) {
20 | resp, err := http.Get(oas3url) // #nosec G107
21 | if err != nil {
22 | return nil, err
23 | }
24 | bytes, err := io.ReadAll(resp.Body)
25 | if err != nil {
26 | return nil, err
27 | }
28 | return Parse(bytes)
29 | }
30 |
31 | // ReadFile does optional validation which is useful when
32 | // merging incomplete spec files.
33 | func ReadFile(oas3file string, validate bool) (*Spec, error) {
34 | if validate {
35 | return readAndValidateFile(oas3file)
36 | }
37 | bytes, err := os.ReadFile(oas3file)
38 | if err != nil {
39 | return nil, errorsutil.Wrapf(err, "ReadFile.ReadFile.Error.Filename file: (%v)", oas3file)
40 | }
41 | if rxYamlExtension.MatchString(oas3file) {
42 | bytes, err = yaml.YAMLToJSON(bytes)
43 | if err != nil {
44 | return nil, err
45 | }
46 | }
47 | spec := &Spec{}
48 | err = spec.UnmarshalJSON(bytes)
49 | if err != nil {
50 | return nil, errorsutil.Wrapf(err, "error ReadFile.UnmarshalJSON.Error.Filename file: (%s) ", oas3file)
51 | }
52 | return spec, nil
53 | }
54 |
55 | func readAndValidateFile(oas3file string) (*Spec, error) {
56 | data, err := os.ReadFile(oas3file)
57 | if err != nil {
58 | return nil, errorsutil.Wrap(err, "E_READ_FILE_ERROR")
59 | }
60 | return readAndValidateBytes(data)
61 | }
62 |
63 | func readAndValidateBytes(b []byte) (*Spec, error) {
64 | spec, err := oas3.NewLoader().LoadFromData(b)
65 | if err != nil {
66 | return spec, errorsutil.Wrap(err, "error `oas3.NewLoader().LoadFromData(bytes)`")
67 | }
68 | _, err = validateMore(spec)
69 | return spec, err
70 | }
71 |
72 | // Parse will parse a byte array to an `*oas3.Swagger` struct.
73 | // It will use JSON first. If unsuccessful, it will attempt to
74 | // parse it as YAML.
75 | func Parse(oas3Bytes []byte) (*Spec, error) {
76 | spec := &Spec{}
77 | err := spec.UnmarshalJSON(oas3Bytes)
78 | if err != nil {
79 | bytes, err2 := yaml.YAMLToJSON(oas3Bytes)
80 | if err2 != nil {
81 | return spec, err
82 | }
83 | spec = &Spec{}
84 | err3 := spec.UnmarshalJSON(bytes)
85 | return spec, err3
86 | }
87 | return spec, err
88 | }
89 |
90 | type ValidationStatus struct {
91 | Status bool
92 | Message string
93 | Context string
94 | OpenAPI string
95 | }
96 |
97 | func validateMore(spec *Spec) (ValidationStatus, error) {
98 | vs := ValidationStatus{}
99 | if spec == nil {
100 | return vs, ErrSpecNotSet
101 | }
102 | version := ""
103 | if spec.Info == nil {
104 | vs := ValidationStatus{
105 | Context: "#/info",
106 | Message: "expect spec to have info object)",
107 | OpenAPI: spec.OpenAPI}
108 | return vs, fmt.Errorf("E_OPENAPI3_MISSING_KEY [%s]", "info")
109 | } else {
110 | version = strings.TrimSpace(spec.Info.Version)
111 | if len(strings.TrimSpace(version)) == 0 {
112 | version = OASVersionDefault
113 | }
114 | vs.OpenAPI = version
115 |
116 | if len(version) == 0 {
117 | jdata, err := jsonutil.MarshalSimple(spec.Info, "", " ")
118 | if err != nil {
119 | return vs, err
120 | }
121 | vs := ValidationStatus{
122 | Context: "#/info",
123 | Message: fmt.Sprintf("expect Object %s to have key version\nmissing keys:version", string(jdata)),
124 | OpenAPI: spec.OpenAPI}
125 | return vs, fmt.Errorf("E_OPENAPI3_MISSING_KEY [%s]", "info/version")
126 | }
127 | vs.Status = true
128 | }
129 | return vs, nil
130 | }
131 |
132 | func (sm *SpecMore) ValidateMore() (ValidationStatus, error) {
133 | if sm.Spec == nil {
134 | return ValidationStatus{}, ErrSpecNotSet
135 | }
136 | return validateMore(sm.Spec)
137 | }
138 |
139 | func (sm *SpecMore) Validate() error {
140 | jbytes, err := sm.MarshalJSON("", "")
141 | if err != nil {
142 | return err
143 | }
144 | _, err = readAndValidateBytes(jbytes)
145 | return err
146 | }
147 |
--------------------------------------------------------------------------------
/postman2/simple/postman_collection_v2_simple.go:
--------------------------------------------------------------------------------
1 | package simple
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "strings"
7 |
8 | "github.com/grokify/mogo/errors/errorsutil"
9 | "github.com/grokify/mogo/net/http/httputilmore"
10 | "github.com/grokify/spectrum/postman2"
11 | )
12 |
13 | type Collection struct {
14 | Info postman2.CollectionInfo `json:"info"`
15 | Item []*Item `json:"item"`
16 | }
17 |
18 | func NewCollectionFromBytes(data []byte) (Collection, error) {
19 | pman := Collection{}
20 | err := json.Unmarshal(data, &pman)
21 | if err != nil {
22 | err = errorsutil.Wrap(err, "spectrum.postman2.simple.NewCollectionFromBytes << json.Unmarshal")
23 | }
24 | return pman, err
25 | }
26 |
27 | func NewCanonicalCollectionFromBytes(data []byte) (postman2.Collection, error) {
28 | collection, errTry := postman2.NewCollectionFromBytes(data)
29 | if errTry == nil {
30 | collection.Inflate()
31 | return collection, nil
32 | }
33 | simpleCollection, err := NewCollectionFromBytes(data)
34 | if err != nil {
35 | err = errorsutil.Wrap(errTry, err.Error())
36 | err = errorsutil.Wrap(err, "spectrum.postman2.simple.NewCanonicalCollectionFromBytes << NewCollectionFromBytes")
37 | return collection, err
38 | }
39 | collection = simpleCollection.ToCanonical()
40 | collection.Inflate()
41 | return collection, nil
42 | }
43 |
44 | func ReadCanonicalCollection(filepath string) (postman2.Collection, error) {
45 | bytes, err := os.ReadFile(filepath)
46 | if err != nil {
47 | err = errorsutil.Wrap(err, "spectrum.postman2.ReadCanonicalCollection << os.ReadFile")
48 | return postman2.Collection{}, err
49 | }
50 | return NewCanonicalCollectionFromBytes(bytes)
51 | }
52 |
53 | func (col *Collection) ToCanonical() postman2.Collection {
54 | cCollection := postman2.Collection{
55 | Info: col.Info,
56 | Item: []*postman2.Item{}}
57 | for _, folder := range col.Item {
58 | cCollection.Item = append(cCollection.Item, folder.ToCanonical())
59 | }
60 | return cCollection
61 | }
62 |
63 | type Item struct {
64 | Name string `json:"name,omitempty"` // Folder,API
65 | Description string `json:"description,omitempty"` // Folder
66 | Item []*Item `json:"item,omitempty"` // Folder
67 | Event []postman2.Event `json:"event,omitempty"` // API
68 | Request Request `json:"request,omitempty"` // API
69 | }
70 |
71 | func (thisItem *Item) ToCanonical() *postman2.Item {
72 | canRequest := thisItem.Request.ToCanonical()
73 | canItem := &postman2.Item{
74 | Name: thisItem.Name,
75 | Item: []*postman2.Item{},
76 | Event: thisItem.Event,
77 | Request: &canRequest}
78 | thisItem.Description = strings.TrimSpace(thisItem.Description)
79 | if len(thisItem.Description) > 0 {
80 | canItem.Description = &postman2.Description{
81 | Content: thisItem.Description,
82 | Type: httputilmore.ContentTypeTextMarkdown}
83 | }
84 | for _, subItem := range thisItem.Item {
85 | canItem.Item = append(canItem.Item, subItem.ToCanonical())
86 | }
87 | return canItem
88 | }
89 |
90 | type APIItem struct {
91 | Name string `json:"name,omitempty"`
92 | Event []postman2.Event `json:"event,omitempty"`
93 | Request Request `json:"request,omitempty"`
94 | }
95 |
96 | func (apiItem *APIItem) ToCanonical() postman2.Item {
97 | canReq := apiItem.Request.ToCanonical()
98 | return postman2.Item{
99 | Name: apiItem.Name,
100 | Event: apiItem.Event,
101 | Request: &canReq}
102 | }
103 |
104 | type Request struct {
105 | URL string `json:"url,omitempty"`
106 | Method string `json:"method,omitempty"`
107 | Header []postman2.Header `json:"header,omitempty"`
108 | Body postman2.RequestBody `json:"body,omitempty"`
109 | Description string `json:"description,omitempty"`
110 | }
111 |
112 | func (req *Request) ToCanonical() postman2.Request {
113 | pmURL := postman2.NewURL(req.URL)
114 | return postman2.Request{
115 | URL: &pmURL,
116 | Method: req.Method,
117 | Header: req.Header,
118 | Body: &req.Body,
119 | Description: req.Description}
120 | }
121 |
--------------------------------------------------------------------------------
/examples/ringcentral/ringcentral.postman2.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "variables": [],
3 | "info": {
4 | "name": "RingCentral REST API",
5 | "description": "This is a collection of API requests associated with the Basic usage plan in RingCentral Connect Platform for Sandbox Testing.\n\nPlease use a Postman environment with the following variables: RC_SERVER_HOSTNAME, RC_APP_KEY, RC_APP_SECRET, RC_USERNAME, RC_EXTENSION, RC_PASSWORD\n\nTo use these resources, you must ALWAYS start by executing one of the OAuth \"Auth\" Requests first, then you can use any other requests (for the 60 minutes that the access_token you obtained is valid).",
6 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
7 | },
8 | "item": [
9 | {
10 | "name": "Auth",
11 | "description": "YOU MUST UPDATE VALUES IN THE \"Pre-request Script\" section of ALL items in this folder to match your RingCentral Sandbox Account.",
12 | "item": [
13 | {
14 | "name": "Fetch access_token",
15 | "event": [
16 | {
17 | "listen": "test",
18 | "script": {
19 | "type": "text/javascript",
20 | "exec": [
21 | "var jsonData = JSON.parse(responseBody);",
22 | "tests[\"Contains an access_token\"] = jsonData.hasOwnProperty('access_token');",
23 | "",
24 | "tests[\"Status code is 200\"] = responseCode.code === 200;",
25 | "",
26 | "postman.setEnvironmentVariable(\"my_access_token\", jsonData.access_token);"
27 | ]
28 | }
29 | },
30 | {
31 | "listen": "prerequest",
32 | "script": {
33 | "type": "text/javascript",
34 | "exec": [
35 | "// Populate the Postman environment with:",
36 | "// RINGCENTRAL_SERVER_URL, RINGCENTRAL_CLIENT_ID, RINGCENTRAL_CLIENT_SECRET,",
37 | "// RINGCENTRAL_USERNAME, RINGCENTRAL_EXTENSION, RINGCENTRAL_PASSWORD",
38 | "var appKey = environment[\"RINGCENTRAL_CLIENT_ID\"];",
39 | "var appSecret = environment[\"RINGCENTRAL_CLIENT_SECRET\"];",
40 | "var apiKey = encodeBasicAuthHeader(appKey, appSecret);",
41 | "",
42 | "postman.setEnvironmentVariable(\"basic_auth_header\", \"Basic \".concat(apiKey));",
43 | "",
44 | "function encodeBasicAuthHeader(appKey, appSecret) {",
45 | " var apiKey = appKey + ':' + appSecret;",
46 | " return btoa(apiKey);",
47 | "}"
48 | ]
49 | }
50 | }
51 | ],
52 | "request": {
53 | "url": {
54 | "raw": "{{RINGCENTRAL_SERVER_URL}}/restapi/oauth/token",
55 | "auth": null,
56 | "host": [
57 | "{{RINGCENTRAL_SERVER_URL}}"
58 | ],
59 | "path": [
60 | "restapi",
61 | "oauth",
62 | "token"
63 | ]
64 | },
65 | "method": "POST",
66 | "header": [
67 | {
68 | "key": "Authorization",
69 | "value": "{{basic_auth_header}}",
70 | "description": ""
71 | },
72 | {
73 | "key": "Accept",
74 | "value": "application/json",
75 | "description": ""
76 | },
77 | {
78 | "key": "Content-Type",
79 | "value": "application/x-www-form-urlencoded;charset=UTF-8",
80 | "description": ""
81 | }
82 | ],
83 | "body": {
84 | "mode": "urlencoded",
85 | "urlencoded": [
86 | {
87 | "key": "grant_type",
88 | "value": "password",
89 | "type": "text",
90 | "enabled": true
91 | },
92 | {
93 | "key": "username",
94 | "value": "{{RINGCENTRAL_USERNAME}}",
95 | "type": "text",
96 | "enabled": true
97 | },
98 | {
99 | "key": "password",
100 | "value": "{{RINGCENTRAL_PASSWORD}}",
101 | "type": "text",
102 | "enabled": true
103 | },
104 | {
105 | "key": "extension",
106 | "value": "{{RINGCENTRAL_EXTENSION}}",
107 | "type": "text",
108 | "enabled": true
109 | }
110 | ]
111 | },
112 | "description": "Fetch access_token from DevTest environment as Admin"
113 | },
114 | "response": []
115 | }
116 | ]
117 | }
118 | ]
119 | }
--------------------------------------------------------------------------------
/postman2/collection.go:
--------------------------------------------------------------------------------
1 | package postman2
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "strings"
7 |
8 | "github.com/grokify/mogo/errors/errorsutil"
9 | "github.com/grokify/mogo/net/http/httputilmore"
10 | )
11 |
12 | const (
13 | SchemaURL210 = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
14 | SchemaURL200 = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
15 | )
16 |
17 | type Collection struct {
18 | Info CollectionInfo `json:"info"`
19 | Item []*Item `json:"item"`
20 | Event []Event `json:"event,omitempty"`
21 | }
22 |
23 | func ReadFile(filename string) (Collection, error) {
24 | b, err := os.ReadFile(filename)
25 | if err != nil {
26 | return Collection{}, err
27 | }
28 | return NewCollectionFromBytes(b)
29 | }
30 |
31 | func NewCollectionFromBytes(data []byte) (Collection, error) {
32 | col := Collection{}
33 | err := json.Unmarshal(data, &col)
34 | if err != nil {
35 | err = errorsutil.Wrap(err, "spectrum.postman2.NewCollectionFromBytes << json.Unmarshal")
36 | return col, err
37 | }
38 | col.Inflate()
39 | return col, nil
40 | }
41 |
42 | func (col *Collection) GetOrNewFolder(folderName string) *Item {
43 | for _, folder := range col.Item {
44 | if folder.Name == folderName {
45 | return folder
46 | }
47 | }
48 | folder := &Item{Name: folderName, Item: []*Item{}}
49 | col.Item = append(col.Item, folder)
50 | return folder
51 | }
52 |
53 | func (col *Collection) SetFolder(newFolder *Item) {
54 | if newFolder == nil || len(strings.TrimSpace(newFolder.Name)) == 0 {
55 | return
56 | }
57 | for i, folder := range col.Item {
58 | if newFolder.Name == folder.Name {
59 | col.Item[i] = newFolder
60 | return
61 | }
62 | }
63 | col.Item = append(col.Item, newFolder)
64 | }
65 |
66 | func (col *Collection) Inflate() {
67 | col.Info.Schema = strings.TrimSpace(col.Info.Schema)
68 | if len(col.Info.Schema) == 0 {
69 | col.Info.Schema = SchemaURL210
70 | }
71 | col.InflateRawURLs()
72 | }
73 |
74 | func (col *Collection) InflateRawURLs() {
75 | for _, folder := range col.Item {
76 | for j, api := range folder.Item {
77 | if api.Request.URL.IsRawOnly() &&
78 | len(strings.TrimSpace(api.Request.URL.Raw)) > 0 {
79 | url := NewURL(strings.TrimSpace(api.Request.URL.Raw))
80 | url.Auth = api.Request.URL.Auth
81 | url.Variable = api.Request.URL.Variable
82 | folder.Item[j].Request.URL = &url
83 | }
84 | }
85 | }
86 | }
87 |
88 | type CollectionInfo struct {
89 | Name string `json:"name,omitempty"`
90 | PostmanID string `json:"_postman_id,omitempty"`
91 | Description string `json:"description,omitempty"`
92 | Schema string `json:"schema,omitempty"`
93 | }
94 |
95 | // Item can represent a folder or an API
96 | type Item struct {
97 | Name string `json:"name,omitempty"` // Folder,Operation
98 | Description *Description `json:"description,omitempty"` // Folder
99 | Item []*Item `json:"item,omitempty"` // Folder
100 | IsSubFolder bool `json:"_postman_isSubFolder,omitempty"` // Folder
101 | Event []Event `json:"event,omitempty"` // Operation
102 | Request *Request `json:"request,omitempty"` // Operation
103 | }
104 |
105 | func (item *Item) UpsertSubItem(newItem *Item) {
106 | if newItem == nil || len(strings.TrimSpace(newItem.Name)) == 0 {
107 | return
108 | }
109 | for i, itemTry := range item.Item {
110 | if itemTry.Name == newItem.Name {
111 | item.Item[i] = newItem
112 | return
113 | }
114 | }
115 | item.Item = append(item.Item, newItem)
116 | }
117 |
118 | type Description struct {
119 | Content string `json:"content,omitempty"`
120 | Type string `json:"type,omitempty"`
121 | Version string `json:"version,omitempty"`
122 | }
123 |
124 | func (desc *Description) Inflate() {
125 | desc.Content = strings.TrimSpace(desc.Content)
126 | desc.Type = strings.TrimSpace(desc.Type)
127 | if len(desc.Content) > 0 && len(desc.Type) == 0 {
128 | desc.Type = httputilmore.ContentTypeTextPlain
129 | }
130 | }
131 |
132 | type Event struct {
133 | Listen string `json:"listen"`
134 | Script Script `json:"script"`
135 | }
136 |
137 | type Script struct {
138 | Type string `json:"type,omitempty"`
139 | Exec []string `json:"exec,omitempty"`
140 | }
141 |
--------------------------------------------------------------------------------
/raml08/raml08openapi3/operations.go:
--------------------------------------------------------------------------------
1 | package raml08openapi3
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "strings"
9 |
10 | oas3 "github.com/getkin/kin-openapi/openapi3"
11 | "github.com/grokify/mogo/net/http/httputilmore"
12 | "github.com/grokify/mogo/net/urlutil"
13 | "github.com/grokify/spectrum/openapi3"
14 | )
15 |
16 | // ReadFileOperations reads a RAML v0.8 file and returns a set of `openapi3edit.OperationMore` structs.
17 | // The properties `path`, `method`, `summary`, `description` are populated. OpenAPI `summary` is populated
18 | // by the `displayName` property. Currently, this reads a JSON formatted file into a map[string]interface.
19 | // This is useful after converting a RAML v0.8 spec using https://github.com/daviemakz/oas-raml-converter-cli.
20 | func ReadFileOperations(filename string) (*openapi3.OperationMores, error) {
21 | bytes, err := os.ReadFile(filename)
22 | if err != nil {
23 | return nil, err
24 | }
25 | msa := map[string]any{}
26 | err = json.Unmarshal(bytes, &msa)
27 | if err != nil {
28 | return nil, err
29 | }
30 | // omSet := &openapi3.OperationMoreSet{
31 | // OperationMores: []openapi3.OperationMore{}}
32 | oms := &openapi3.OperationMores{}
33 | err = msaPaths("", msa, oms)
34 | return oms, err
35 | }
36 |
37 | const (
38 | RAMLKeyDescription = "description"
39 | RAMLKeyDisplayName = "displayName"
40 | )
41 |
42 | var (
43 | ErrOperationMoreSetMissing = errors.New("required parameter operationSetMore is empty")
44 | ErrRAMLDescriptionNotString = errors.New("format for RAML description is not string")
45 | ErrRAMLDispNameNotString = errors.New("format for RAML description is not string")
46 | )
47 |
48 | // msaPaths is a recursive function. Use "" for the basePath for RAML root. The structure of RAML
49 | // appears to be that properties for metadata and sub-paths are co-mingled when sub-paths starting
50 | // with a slash `/`. This method walks the operation segements and collects the following information
51 | // using the `openapi3edit.OperationMore` struct: `path`, `method`, `summary`, `description`. OpenAPI
52 | // `summary` is populated by the `displayName` property.
53 | func msaPaths(basePath string, msa map[string]any, oms *openapi3.OperationMores) error {
54 | if len(msa) == 0 {
55 | return nil
56 | } else if oms == nil {
57 | return ErrOperationMoreSetMissing
58 | }
59 | basePath = strings.TrimSpace(basePath)
60 | if len(basePath) > 0 {
61 | // only do if not at root.
62 | pathOms, err := operationMoresFromPathItem(basePath, msa)
63 | if err != nil {
64 | return err
65 | } else if len(pathOms) > 0 {
66 | *oms = append(*oms, pathOms...)
67 | }
68 | }
69 |
70 | for k, msaAny := range msa {
71 | k = strings.TrimSpace(k)
72 | if len(k) == 0 || strings.Index(k, "/") != 0 {
73 | continue
74 | }
75 | childMSA, ok := msaAny.(map[string]any)
76 | if !ok {
77 | return fmt.Errorf("value is not map[string]any for key [%s]", k)
78 | }
79 | childAbsPath := urlutil.JoinAbsolute(basePath, k)
80 | err := msaPaths(childAbsPath, childMSA, oms)
81 | if err != nil {
82 | return err
83 | }
84 | }
85 | return nil
86 | }
87 |
88 | func operationMoresFromPathItem(opPath string, opPathItem map[string]any) ([]openapi3.OperationMore, error) {
89 | oms := []openapi3.OperationMore{}
90 | for k, valAny := range opPathItem {
91 | // check if current `opPathItem`` property is an HTTP method, and add operations if so.
92 | methodCanonical, err := httputilmore.ParseHTTPMethod(k)
93 | if err != nil { // err means not known HTTP Method
94 | continue
95 | }
96 | om := openapi3.OperationMore{
97 | Path: opPath,
98 | Method: string(methodCanonical),
99 | Operation: &oas3.Operation{}}
100 | opMSA := valAny.(map[string]any)
101 | if descAny, ok := opMSA[RAMLKeyDescription]; ok {
102 | if descStr, ok := descAny.(string); ok {
103 | om.Operation.Description = strings.TrimSpace(descStr)
104 | } else {
105 | return oms, ErrRAMLDescriptionNotString
106 | }
107 | }
108 | if dispNameAny, ok := opMSA[RAMLKeyDisplayName]; ok {
109 | if dispNameStr, ok := dispNameAny.(string); ok {
110 | om.Operation.Summary = strings.TrimSpace(dispNameStr)
111 | } else {
112 | return oms, ErrRAMLDispNameNotString
113 | }
114 | }
115 | oms = append(oms, om)
116 | }
117 | return oms, nil
118 | }
119 |
--------------------------------------------------------------------------------
/openapi3/openapi3html/apiregistry/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "os"
7 | "strings"
8 |
9 | "github.com/buaazp/fasthttprouter"
10 | "github.com/grokify/mogo/log/logutil"
11 | "github.com/grokify/mogo/net/http/httputilmore"
12 | "github.com/grokify/mogo/strconv/strconvutil"
13 | "github.com/grokify/mogo/type/stringsutil"
14 | "github.com/grokify/sogo/net/http/anyhttp"
15 | "github.com/grokify/sogo/net/http/httpsimple"
16 | "github.com/grokify/spectrum/openapi3"
17 | "github.com/grokify/spectrum/openapi3/openapi3html"
18 | "github.com/rs/zerolog"
19 | "github.com/rs/zerolog/log"
20 | "github.com/valyala/fasthttp"
21 | )
22 |
23 | type Server struct {
24 | Port int
25 | Engine string
26 | }
27 |
28 | func NewServer() Server {
29 | return Server{
30 | Port: strconvutil.AtoiOrDefault(os.Getenv("PORT"), 8080),
31 | Engine: stringsutil.FirstNonEmpty(os.Getenv("ENGINE"), "nethttp")}
32 | }
33 |
34 | func (svr *Server) HandleAPIRegistryNetHTTP(res http.ResponseWriter, req *http.Request) {
35 | log.Debug().Msg("FUNC_HandleNetHTTP__BEGIN")
36 | svr.HandleAPIRegistryAnyEngine(anyhttp.NewResReqNetHTTP(res, req))
37 | }
38 |
39 | func (svr *Server) HandleAPIRegistryFastHTTP(ctx *fasthttp.RequestCtx) {
40 | log.Debug().Msg("HANDLE_FastHTTP")
41 | svr.HandleAPIRegistryAnyEngine(anyhttp.NewResReqFastHTTP(ctx))
42 | }
43 |
44 | func (svr *Server) HandleAPIRegistryAnyEngine(aRes anyhttp.Response, aReq anyhttp.Request) {
45 | log.Debug().Msg("FUNC_HandleAnyEngine__BEGIN")
46 | aRes.SetContentType(httputilmore.ContentTypeTextHTMLUtf8)
47 | err := aReq.ParseForm()
48 | if err != nil {
49 | logutil.PrintErr(SetResponseError(aRes, err.Error()))
50 | return
51 | }
52 |
53 | specURL := strings.TrimSpace(aReq.QueryArgs().GetString("url"))
54 | log.Debug().
55 | Str("url", specURL).
56 | Msg("SpecURL")
57 |
58 | if len(specURL) == 0 {
59 | logutil.PrintErr(SetResponseError(aRes, "No OpenAPI 3.0 Spec URL"))
60 | return
61 | }
62 | resp, err := http.Get(specURL) // #nosec G107
63 | if err != nil || resp.StatusCode > 299 {
64 | logutil.PrintErr(SetResponseError(aRes, err.Error()))
65 | return
66 | }
67 |
68 | bytes, err := io.ReadAll(resp.Body)
69 | if err != nil {
70 | logutil.PrintErr(SetResponseError(aRes, err.Error()))
71 | return
72 | }
73 |
74 | spec, err := openapi3.Parse(bytes)
75 | if err != nil {
76 | logutil.PrintErr(SetResponseError(aRes, err.Error()))
77 | return
78 | }
79 |
80 | oas3HTMLParams := openapi3html.PageParams{
81 | PageTitle: spec.Info.Title,
82 | TableDomID: "specTable",
83 | Spec: spec}
84 |
85 | oas3PageHTML := openapi3html.SpectrumUIPage(oas3HTMLParams)
86 |
87 | aRes.SetHeader(httputilmore.HeaderContentType, httputilmore.ContentTypeTextHTMLUtf8)
88 | _, err = aRes.SetBodyBytes([]byte(oas3PageHTML))
89 | logutil.PrintErr(err)
90 | }
91 |
92 | func SetResponseError(aRes anyhttp.Response, bodyText string) error {
93 | bodyText = `
94 |
95 |
96 | ` + bodyText + `
`
97 |
98 | _, err := aRes.SetBodyBytes([]byte(bodyText))
99 | return err
100 | }
101 |
102 | /*
103 | const ErrorPage = `
104 |
105 | Error
106 | `
107 | */
108 |
109 | func (svr Server) PortInt() int { return svr.Port }
110 | func (svr Server) HTTPEngine() string { return svr.Engine }
111 | func (svr Server) RouterFast() *fasthttprouter.Router { return nil }
112 |
113 | func (svr Server) Router() http.Handler {
114 | /*
115 | mux := mux.NewRouter()
116 | mux.HandleFunc("/ping", http.HandlerFunc(httpsimple.HandleTestNetHTTP))
117 | mux.HandleFunc("/ping/", http.HandlerFunc(httpsimple.HandleTestNetHTTP))
118 | mux.HandleFunc("/", http.HandlerFunc(svr.HandleAPIRegistryNetHTTP))
119 | return mux
120 | */
121 | mux := http.NewServeMux()
122 | mux.HandleFunc("/", http.HandlerFunc(svr.HandleAPIRegistryNetHTTP))
123 | mux.HandleFunc("/ping", http.HandlerFunc(httpsimple.HandleTestNetHTTP))
124 | mux.HandleFunc("/ping/", http.HandlerFunc(httpsimple.HandleTestNetHTTP))
125 | return mux
126 | }
127 |
128 | func main() {
129 | zerolog.SetGlobalLevel(zerolog.DebugLevel)
130 |
131 | svr := NewServer()
132 |
133 | done := make(chan bool)
134 | go httpsimple.Serve(svr)
135 | log.Info().Int("port", svr.Port).Msg("Server listening")
136 | <-done
137 | }
138 |
--------------------------------------------------------------------------------
/postman2/url.go:
--------------------------------------------------------------------------------
1 | package postman2
2 |
3 | import (
4 | "net/url"
5 | "regexp"
6 | "strings"
7 |
8 | "github.com/grokify/mogo/net/urlutil"
9 | )
10 |
11 | // URL is the Postman URL used in the Postman 2.0 Collection Spec.
12 | type URL struct {
13 | Raw string `json:"raw,omitempty"`
14 | Protocol string `json:"protocol,omitempty"`
15 | Auth map[string]string `json:"auth,omitempty"`
16 | Host []string `json:"host,omitempty"`
17 | Path []string `json:"path,omitempty"`
18 | Query []URLQuery `json:"query,omitempty"`
19 | Variable []URLVariable `json:"variable,omitempty"`
20 | }
21 |
22 | // URLParameters is a temp struct to hold parsed parameters.
23 | type URLParameters struct {
24 | Query []URLQuery `json:"query,omitempty"`
25 | Variable []URLVariable `json:"variable,omitempty"`
26 | }
27 |
28 | // NewURLParameters returns an initialized empty struct.
29 | func NewURLParameters() URLParameters {
30 | return URLParameters{
31 | Query: []URLQuery{},
32 | Variable: []URLVariable{},
33 | }
34 | }
35 |
36 | func (pmURL *URL) IsRawOnly() bool {
37 | pmURL.Protocol = strings.TrimSpace(pmURL.Protocol)
38 | if len(pmURL.Protocol) > 0 ||
39 | len(pmURL.Host) > 0 ||
40 | len(pmURL.Path) > 0 {
41 | return false
42 | }
43 | return true
44 | }
45 |
46 | type URLQuery struct {
47 | Key string `json:"key,omitempty"`
48 | Value string `json:"value,omitempty"`
49 | Description string `json:"description,omitempty"`
50 | Disabled bool `json:"disabled,omitempty"`
51 | }
52 |
53 | type URLVariable struct {
54 | Key string `json:"key,omitempty"`
55 | Value interface{} `json:"value,omitempty"`
56 | Description URLVariableDescription `json:"description,omitempty"`
57 | Disabled bool `json:"disabled,omitempty"`
58 | ID string `json:"id,omitempty"` // Old, pre 2.0.1
59 | }
60 |
61 | type URLVariableDescription struct {
62 | Content string `json:"content,omitempty"`
63 | Type string `json:"type,omitempty"`
64 | }
65 |
66 | func NewURLForGoURL(goURL url.URL) URL {
67 | pmURL := URL{Variable: []URLVariable{}}
68 | goURL.Scheme = strings.TrimSpace(goURL.Scheme)
69 | goURL.Host = strings.TrimSpace(goURL.Host)
70 | goURL.Path = strings.TrimSpace(goURL.Path)
71 | urlParts := []string{}
72 | if len(goURL.Host) > 0 {
73 | pmURL.Host = strings.Split(goURL.Host, ".")
74 | urlParts = append(urlParts, goURL.Host)
75 | }
76 | if len(goURL.Path) > 0 {
77 | pmURL.Path = strings.Split(goURL.Path, "/")
78 | urlParts = append(urlParts, goURL.Path)
79 | }
80 | rawURL := strings.Join(urlParts, "/")
81 | if len(goURL.Scheme) > 0 {
82 | pmURL.Protocol = goURL.Scheme
83 | rawURL = goURL.Scheme + "://" + rawURL
84 | }
85 | pmURL.Raw = rawURL
86 | return pmURL
87 | }
88 |
89 | var simpleURLRx = regexp.MustCompile(`^([a-z][0-9a-z]+)://([^/]+)/(.*)$`)
90 |
91 | func NewURL(rawURL string) URL {
92 | rawURL = strings.TrimSpace(rawURL)
93 | pmURL := URL{Raw: rawURL, Variable: []URLVariable{}}
94 | rs1 := simpleURLRx.FindAllStringSubmatch(rawURL, -1)
95 |
96 | if len(rs1) > 0 {
97 | for _, m := range rs1 {
98 | pmURL.Protocol = m[1]
99 | hostname := m[2]
100 | path := m[3]
101 | pmURL.Host = strings.Split(hostname, ".")
102 | pmURL.Path = urlutil.SplitPath(path, true, true)
103 | }
104 | } else if strings.Index(rawURL, "{") == 0 {
105 | parts := urlutil.SplitPath(rawURL, true, true)
106 | if len(parts) > 0 {
107 | pmURL.Host = []string{parts[0]}
108 | }
109 | if len(parts) > 1 {
110 | pmURL.Path = parts[1:]
111 | }
112 | }
113 |
114 | return pmURL
115 | }
116 |
117 | // AddVariable adds a Postman Variable to the struct.
118 | func (pmURL *URL) AddVariable(key string, value interface{}) {
119 | variable := URLVariable{ID: key, Value: value}
120 | pmURL.Variable = append(pmURL.Variable, variable)
121 | }
122 |
123 | const (
124 | apiURLOasToPostmanVarMatch string = `(^|[^\{])\{([^\/\{\}]+)\}([^\}]|$)`
125 | apiURLOasToPostmanVarReplace string = "$1:$2$3"
126 | )
127 |
128 | var apiURLOasToPostmanVarMatchRx = regexp.MustCompile(
129 | apiURLOasToPostmanVarMatch)
130 |
131 | func APIURLOasToPostman(urlWithOasVars string) string {
132 | return apiURLOasToPostmanVarMatchRx.ReplaceAllString(
133 | urlWithOasVars, apiURLOasToPostmanVarReplace)
134 | }
135 |
--------------------------------------------------------------------------------
/ext/taggroups/taggroups.go:
--------------------------------------------------------------------------------
1 | package taggroups
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "reflect"
7 | "strings"
8 |
9 | "github.com/grokify/mogo/type/stringsutil"
10 | "github.com/grokify/spectrum/openapi3"
11 | "github.com/grokify/spectrum/openapi3edit"
12 | )
13 |
14 | const (
15 | XTagGroupsPropertyNameRedocly = "x-tagGroups" // Redocly format
16 | XTagGroupsPropertyNameKebab = "x-tag-groups" // Alternate format
17 | XTagGroupsPropertyName = XTagGroupsPropertyNameRedocly
18 | )
19 |
20 | type TagGroupSet struct {
21 | TagGroups []TagGroup
22 | }
23 |
24 | func NewTagGroupSet() TagGroupSet {
25 | return TagGroupSet{TagGroups: []TagGroup{}}
26 | }
27 |
28 | func (set *TagGroupSet) Exists(tagName string) bool {
29 | for _, tg := range set.TagGroups {
30 | for _, tgTagName := range tg.Tags {
31 | if tagName == tgTagName {
32 | return true
33 | }
34 | }
35 | }
36 | return false
37 | }
38 |
39 | func (set *TagGroupSet) GetTagGroupNamesForTagNames(wantTagNames ...string) []string {
40 | tagGroupNames := []string{}
41 | for _, tg := range set.TagGroups {
42 | for _, tgTagName := range tg.Tags {
43 | for _, wantTagName := range wantTagNames {
44 | if wantTagName == tgTagName {
45 | tagGroupNames = append(tagGroupNames, tg.Name)
46 | }
47 | }
48 | }
49 | }
50 | return stringsutil.SliceCondenseSpace(tagGroupNames, true, true)
51 | }
52 |
53 | func (set *TagGroupSet) AddToSpec(spec *openapi3.Spec) error {
54 | if len(set.TagGroups) == 0 {
55 | return nil
56 | }
57 | missing := SpecTagsWithoutGroups(spec, *set)
58 | if len(missing) > 0 {
59 | return fmt.Errorf("E_TAGS_WITHOUT_GROUPS [%s]", strings.Join(missing, ","))
60 | }
61 | se := openapi3edit.SpecEdit{}
62 | se.SpecSet(spec)
63 | se.ExtensionSet(XTagGroupsPropertyName, set.TagGroups)
64 | // spec.ExtensionProps.Extensions[XTagGroupsPropertyName] = set.TagGroups
65 | return nil
66 | }
67 |
68 | // OperationMoreTagGroupNames this function is meant to be used with `SpecMore.Table()`
69 | // and must follow the `OperationMoreStringFunc` interface.
70 | func (set *TagGroupSet) OperationMoreTagGroupNames(opm *openapi3.OperationMore) string {
71 | // row = append(row, strings.Join(tgs.GetTagGroupNamesForTagNames(op.Tags...), ", "))
72 | if opm == nil || opm.Operation == nil {
73 | return ""
74 | }
75 | return strings.Join(set.GetTagGroupNamesForTagNames(opm.Operation.Tags...), ", ")
76 | }
77 |
78 | type TagGroup struct {
79 | Name string `json:"name"`
80 | Popular bool `json:"popular"`
81 | Tags []string `json:"tags"`
82 | }
83 |
84 | /*
85 | func (sm *SpecMore) TagsWithoutGroups() ([]string, []string, []string, error) {
86 | tgs, err := sm.TagGroups()
87 | if err != nil {
88 | return []string{}, []string{}, []string{}, err
89 | }
90 | allTags := []string{}
91 |
92 | topTags := stringsutil.SliceCondenseSpace(sm.Tags(true, false), true, true)
93 | allTags = append(allTags, topTags...)
94 |
95 | opsTags := stringsutil.SliceCondenseSpace(sm.Tags(false, true), true, true)
96 | allTags = append(allTags, opsTags...)
97 |
98 | allTags = stringsutil.SliceCondenseSpace(allTags, true, true)
99 | return allTags, topTags, opsTags, nil
100 | }
101 | */
102 |
103 | func SpecTagsWithoutGroups(spec *openapi3.Spec, tagGroupSet TagGroupSet) []string {
104 | missing := []string{}
105 | for _, tag := range spec.Tags {
106 | if !tagGroupSet.Exists(tag.Name) {
107 | missing = append(missing, tag.Name)
108 | }
109 | }
110 | return missing
111 | }
112 |
113 | // SpecTagGroups parses a TagGroupSet from an OpenAPI3 spec.
114 | func SpecTagGroups(spec *openapi3.Spec) (TagGroupSet, error) {
115 | sm := openapi3.SpecMore{Spec: spec}
116 | tgs := NewTagGroupSet()
117 | // iface, ok := sm.Spec.ExtensionProps.Extensions[XTagGroupsPropertyName]
118 | iface, ok := sm.Spec.Extensions[XTagGroupsPropertyName]
119 | if !ok {
120 | return tgs, nil
121 | }
122 |
123 | tagGroups := []TagGroup{}
124 | if reflect.TypeOf(iface) == reflect.TypeOf(tagGroups) {
125 | tgs.TagGroups = iface.([]TagGroup)
126 | return tgs, nil
127 | }
128 |
129 | // message is stored as `json.RawMessage` when the data
130 | // is read in from JSON, vs. set via code.
131 | rawMessage := iface.(json.RawMessage)
132 | err := json.Unmarshal(rawMessage, &tagGroups)
133 | if err != nil {
134 | return tgs, err
135 | }
136 | tgs.TagGroups = tagGroups
137 | // delete(sm.Spec.ExtensionProps.Extensions, XTagGroupsPropertyName)
138 | // sm.Spec.ExtensionProps.Extensions[XTagGroupsPropertyName] = tagGroups
139 | delete(sm.Spec.Extensions, XTagGroupsPropertyName)
140 | sm.Spec.Extensions[XTagGroupsPropertyName] = tagGroups
141 | return tgs, nil
142 | }
143 |
--------------------------------------------------------------------------------
/openapi3edit/schemas_flatten.go:
--------------------------------------------------------------------------------
1 | package openapi3edit
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strings"
7 |
8 | oas3 "github.com/getkin/kin-openapi/openapi3"
9 | "github.com/grokify/mogo/type/stringsutil"
10 | "github.com/grokify/spectrum/openapi3"
11 | )
12 |
13 | func (se *SpecEdit) SchemasFlatten() {
14 | if se.SpecMore.Spec == nil {
15 | return
16 | }
17 | for schName, schRef := range se.SpecMore.Spec.Components.Schemas {
18 | if schRef == nil || schRef.Value == nil {
19 | continue
20 | }
21 | se.SchemasFlattenSchemaRef("", schName, schRef)
22 | }
23 | }
24 |
25 | func (se *SpecEdit) SchemasFlattenSchemaRef(baseName, schName string, schRef *oas3.SchemaRef) {
26 | if se.SpecMore.Spec == nil || schRef == nil {
27 | return
28 | }
29 | spec := se.SpecMore.Spec
30 |
31 | basePlusSchName := baseName + stringsutil.ToUpperFirst(schName, false)
32 | if len(baseName) == 0 {
33 | basePlusSchName = schName
34 | }
35 |
36 | for propName, propRef := range schRef.Value.Properties {
37 | if propRef == nil || propRef.Value == nil {
38 | continue
39 | }
40 | if openapi3.TypesRefIs(propRef.Value.Type, openapi3.TypeArray) {
41 | itemsRef := propRef.Value.Items
42 | if itemsRef == nil {
43 | continue
44 | }
45 | itemsRef.Ref = strings.TrimSpace(itemsRef.Ref)
46 | if itemsRef.Value != nil &&
47 | (openapi3.TypesRefIs(itemsRef.Value.Type, openapi3.TypeArray, openapi3.TypeObject)) {
48 | if len(itemsRef.Ref) > 0 {
49 | propRef.Value.Items = oas3.NewSchemaRef(itemsRef.Ref, nil)
50 | } else {
51 | newSchemaName := basePlusSchName + stringsutil.ToUpperFirst(propName, false)
52 | if _, ok := spec.Components.Schemas[newSchemaName]; ok {
53 | fmt.Printf("BASE_NAME [%s] SCH_NAME [%s] PROP_NAME [%s] ARRAY\n", baseName, schName, propName)
54 | panic("collision")
55 | }
56 | se.SchemasFlattenSchemaRef(basePlusSchName, propName, itemsRef)
57 | spec.Components.Schemas[newSchemaName] = itemsRef
58 | propRef.Value.Items = oas3.NewSchemaRef(openapi3.PointerComponentsSchemas+"/"+newSchemaName, nil)
59 | }
60 | //itemsRef.Value = nil
61 | //itemsRef.Ref = openapi3.PointerComponentsSchemas + "/" + newSchemaName
62 | }
63 | //} else if propRef.Value.Type == openapi3.TypeObject {
64 | } else if openapi3.TypesRefIs(propRef.Value.Type, openapi3.TypeObject) {
65 | if len(propRef.Value.Properties) > 0 {
66 | newSchemaName := basePlusSchName + stringsutil.ToUpperFirst(propName, false)
67 | if _, ok := spec.Components.Schemas[newSchemaName]; ok {
68 | fmt.Printf("BASE_NAME [%s] SCH_NAME [%s] PROP_NAME [%s] OBJECT\n", baseName, schName, propName)
69 | panic("collision")
70 | }
71 | //SpecSchemasFlattenSchemaRef(spec, basePlusSchName, propName, propRef)
72 | se.SchemasFlattenSchemaRef("", newSchemaName, propRef)
73 | spec.Components.Schemas[newSchemaName] = propRef
74 | schRef.Value.Properties[propName] = oas3.NewSchemaRef(openapi3.PointerComponentsSchemas+"/"+newSchemaName, nil)
75 | //SpecSchemasFlattenSchemaRef(spec, basePlusSchName, propName, propRef)
76 | //schRef.Value.Properties[propName] = oas3.NewSchemaRef(newSchemaName, nil)
77 | }
78 | }
79 | }
80 | }
81 |
82 | var ErrEmptySchemaName = errors.New("empty schema name encountered")
83 |
84 | // SchemaRefsFlatten flattens Schema refs.
85 | func (se *SpecEdit) SchemaRefsFlatten() error {
86 | if se.SpecMore.Spec == nil {
87 | return openapi3.ErrSpecNotSet
88 | }
89 | spec := se.SpecMore.Spec
90 | // func SpecFlattenSchemaRefs(spec *openapi3.Spec, visitSchemaRefFunc func(schName string, schRef *oas3.SchemaRef) error) error {
91 | for schName, schRef := range spec.Components.Schemas {
92 | for propSchemaName, propSchemaRef := range schRef.Value.Properties {
93 | if len(strings.TrimSpace(propSchemaName)) == 0 {
94 | return ErrEmptySchemaName
95 | }
96 | // visitSchemaRefFunc(propSchemaName, propSchemaRef)
97 | if len(propSchemaRef.Ref) == 0 && propSchemaRef.Value != nil {
98 | if openapi3.TypesRefIs(propSchemaRef.Value.Type, openapi3.TypeObject, openapi3.TypeArray) {
99 | newRootSchemaName := propSchemaName
100 | if _, ok := spec.Components.Schemas[newRootSchemaName]; ok {
101 | newRootSchemaName = schName + stringsutil.ToUpperFirst(newRootSchemaName, false)
102 | if _, ok := spec.Components.Schemas[newRootSchemaName]; ok {
103 | return fmt.Errorf("schema collision [%s]", newRootSchemaName)
104 | }
105 | }
106 | spec.Components.Schemas[newRootSchemaName] = propSchemaRef
107 | propSchemaRef.Value = nil
108 | propSchemaRef.Ref = openapi3.PointerComponentsSchemas + "/" + newRootSchemaName
109 | }
110 | }
111 | }
112 | }
113 | return nil
114 | }
115 |
--------------------------------------------------------------------------------