├── internal ├── e2etest │ ├── testdata │ │ ├── minimal │ │ │ ├── petstore.yaml │ │ │ ├── output.jsonrpc │ │ │ └── input.jsonrpc │ │ ├── definition │ │ │ ├── petstore.yaml │ │ │ ├── output.jsonrpc │ │ │ └── input.jsonrpc │ │ └── references │ │ │ ├── petstore.yaml │ │ │ ├── output.jsonrpc │ │ │ └── input.jsonrpc │ ├── README.md │ └── e2etest_test.go ├── analysis │ ├── yaml │ │ ├── doc.go │ │ ├── yaml.go │ │ ├── yaml_test.go │ │ └── testdata │ │ │ └── petstore.yaml │ ├── doc.go │ ├── handler.go │ └── handler_test.go └── lsp │ ├── testutil │ ├── doc.go │ └── handler.go │ ├── jsonrpc │ ├── doc.go │ ├── jsonrpc.go │ └── jsonrpc_test.go │ ├── doc.go │ ├── types │ ├── doc.go │ ├── language_features.go │ ├── base_protocol_test.go │ ├── lifecycle_messages.go │ ├── base_protocol.go │ ├── basic_json_structures.go │ └── document_synchronization.go │ ├── utf16.go │ ├── handler.go │ ├── file.go │ ├── file_test.go │ ├── server.go │ └── server_test.go ├── .github ├── .release-please-manifest.json ├── release-please-config.json ├── dependabot.yaml ├── .goreleaser.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── .gitattributes ├── go.mod ├── go.sum ├── generate.go ├── LICENSE ├── CHANGELOG.md ├── README.md ├── main.go └── .golangci.yaml /internal/e2etest/testdata/minimal/petstore.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {".":"0.2.2"} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | /openapiv3-lsp 3 | /dist 4 | /tmp 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force crlf on testdata since crlf is part of the JSON-RPC protocol. 2 | *.jsonrpc eol=crlf 3 | -------------------------------------------------------------------------------- /internal/analysis/yaml/doc.go: -------------------------------------------------------------------------------- 1 | // Package yaml provides parsing and analysis of YAML documents. 2 | package yaml 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/armsnyder/openapi-language-server 2 | 3 | go 1.22.4 4 | 5 | require go.uber.org/mock v0.4.0 6 | -------------------------------------------------------------------------------- /internal/analysis/doc.go: -------------------------------------------------------------------------------- 1 | // Package analysis contains the OpenAPI Language Server business logic. 2 | package analysis 3 | -------------------------------------------------------------------------------- /internal/lsp/testutil/doc.go: -------------------------------------------------------------------------------- 1 | // Package testutil provides common utilities for testing the LSP server. 2 | package testutil 3 | -------------------------------------------------------------------------------- /internal/lsp/jsonrpc/doc.go: -------------------------------------------------------------------------------- 1 | // Package jsonrpc provides utilities for working with the JSON-RPC protocol. 2 | package jsonrpc 3 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 2 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 3 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source internal/lsp/handler.go -destination internal/lsp/testutil/handler.go -package testutil 4 | -------------------------------------------------------------------------------- /internal/lsp/doc.go: -------------------------------------------------------------------------------- 1 | // Package lsp provides a pluggable language server protocol implementation, as 2 | // well as utilities to make building language servers easier. 3 | package lsp 4 | -------------------------------------------------------------------------------- /internal/lsp/types/doc.go: -------------------------------------------------------------------------------- 1 | // Package types contains the Language Server Protocol specification types. 2 | // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/. 3 | package types 4 | -------------------------------------------------------------------------------- /internal/e2etest/testdata/definition/petstore.yaml: -------------------------------------------------------------------------------- 1 | paths: 2 | /pet: 3 | post: 4 | requestBody: 5 | content: 6 | application/json: 7 | schema: 8 | $ref: "#/components/schemas/Pet" 9 | components: 10 | schemas: 11 | Pet: 12 | type: object 13 | -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "packages": { 4 | ".": { 5 | "release-type": "go", 6 | "bump-minor-pre-major": true, 7 | "include-v-in-tag": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/e2etest/testdata/minimal/output.jsonrpc: -------------------------------------------------------------------------------- 1 | Content-Length: 225 2 | 3 | {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":{"openClose":true,"change":2},"definitionProvider":true,"referencesProvider":true},"serverInfo":{"name":"openapi-language-server","version":"development"}}}Content-Length: 38 4 | 5 | {"jsonrpc":"2.0","id":2,"result":null} -------------------------------------------------------------------------------- /internal/e2etest/testdata/references/petstore.yaml: -------------------------------------------------------------------------------- 1 | paths: 2 | /pet: 3 | post: 4 | requestBody: 5 | content: 6 | application/json: 7 | schema: 8 | $ref: "#/components/schemas/Pet" 9 | application/xml: 10 | schema: 11 | $ref: "#/components/schemas/Pet" 12 | components: 13 | schemas: 14 | Pet: 15 | type: object 16 | -------------------------------------------------------------------------------- /internal/lsp/types/language_features.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#definitionParams. 4 | type DefinitionParams struct { 5 | TextDocumentPositionParams 6 | } 7 | 8 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#referenceParams. 9 | type ReferenceParams struct { 10 | TextDocumentPositionParams 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/dependabot-2.0.json 2 | version: 2 3 | updates: 4 | - directory: / 5 | package-ecosystem: gomod 6 | schedule: 7 | interval: monthly 8 | commit-message: 9 | prefix: chore 10 | include: scope 11 | - directory: / 12 | package-ecosystem: github-actions 13 | schedule: 14 | interval: monthly 15 | commit-message: 16 | prefix: chore 17 | include: scope 18 | -------------------------------------------------------------------------------- /internal/lsp/utf16.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import "unicode/utf8" 4 | 5 | // UTF16Len returns the number of UTF-16 code units required to encode the 6 | // given UTF-8 byte slice. 7 | func UTF16Len(s []byte) int { 8 | n := 0 9 | 10 | for len(s) > 0 { 11 | n++ 12 | 13 | if s[0] < 0x80 { 14 | // ASCII optimization 15 | s = s[1:] 16 | continue 17 | } 18 | 19 | r, size := utf8.DecodeRune(s) 20 | 21 | if r >= 0x10000 { 22 | // UTF-16 surrogate pair 23 | n++ 24 | } 25 | 26 | s = s[size:] 27 | } 28 | 29 | return n 30 | } 31 | -------------------------------------------------------------------------------- /internal/e2etest/testdata/definition/output.jsonrpc: -------------------------------------------------------------------------------- 1 | Content-Length: 225 2 | 3 | {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":{"openClose":true,"change":2},"definitionProvider":true,"referencesProvider":true},"serverInfo":{"name":"openapi-language-server","version":"development"}}}Content-Length: 231 4 | 5 | {"jsonrpc":"2.0","id":2,"result":[{"uri":"file:///Users/adam/repos/armsnyder/openapi-language-server/internal/e2etest/testdata/definition/petstore.yaml","range":{"start":{"line":10,"character":4},"end":{"line":10,"character":7}}}]}Content-Length: 38 6 | 7 | {"jsonrpc":"2.0","id":3,"result":null}Content-Length: 38 8 | 9 | {"jsonrpc":"2.0","id":4,"result":null} -------------------------------------------------------------------------------- /internal/lsp/types/base_protocol_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | . "github.com/armsnyder/openapi-language-server/internal/lsp/types" 8 | ) 9 | 10 | func TestResponseMessage_MarshalJSON(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | message ResponseMessage 14 | want string 15 | }{ 16 | { 17 | name: "empty", 18 | message: ResponseMessage{JSONRPC: "2.0"}, 19 | want: `{"jsonrpc":"2.0","id":null,"result":null}`, 20 | }, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | s, err := json.Marshal(tt.message) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if string(s) != tt.want { 30 | t.Errorf("got %s, want %s", s, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/e2etest/testdata/references/output.jsonrpc: -------------------------------------------------------------------------------- 1 | Content-Length: 225 2 | 3 | {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":{"openClose":true,"change":2},"definitionProvider":true,"referencesProvider":true},"serverInfo":{"name":"openapi-language-server","version":"development"}}}Content-Length: 429 4 | 5 | {"jsonrpc":"2.0","id":2,"result":[{"uri":"file:///Users/adam/repos/armsnyder/openapi-language-server/internal/e2etest/testdata/references/petstore.yaml","range":{"start":{"line":7,"character":21},"end":{"line":7,"character":45}}},{"uri":"file:///Users/adam/repos/armsnyder/openapi-language-server/internal/e2etest/testdata/references/petstore.yaml","range":{"start":{"line":10,"character":21},"end":{"line":10,"character":45}}}]}Content-Length: 38 6 | 7 | {"jsonrpc":"2.0","id":3,"result":null}Content-Length: 38 8 | 9 | {"jsonrpc":"2.0","id":4,"result":null} -------------------------------------------------------------------------------- /.github/.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | 3 | version: 2 4 | 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | goarch: 13 | - amd64 14 | - arm64 15 | ldflags: 16 | - "-X main.version={{ .Version }}" 17 | 18 | universal_binaries: 19 | - replace: false 20 | 21 | archives: 22 | - format: tar.gz 23 | name_template: >- 24 | {{ .ProjectName }}_ 25 | {{- if eq .Os "darwin" }}MacOS 26 | {{- else }}{{ title .Os }}{{ end }}_ 27 | {{- if eq .Arch "amd64" }}x86_64 28 | {{- else if eq .Arch "all" }}universal 29 | {{- else }}{{ .Arch }}{{ end }} 30 | {{- if .Arm }}v{{ .Arm }}{{ end }} 31 | format_overrides: 32 | - goos: windows 33 | format: zip 34 | wrap_in_directory: true 35 | 36 | checksum: 37 | name_template: checksums.txt 38 | 39 | changelog: 40 | disable: true 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adam Snyder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/e2etest/README.md: -------------------------------------------------------------------------------- 1 | # End to end tests 2 | 3 | The **/testdata** directory contains a set of subdirectories, each of which is 4 | an end-to-end test scenario. 5 | 6 | ## Running the tests 7 | 8 | End to end tests only run if the openapi-language-server binary is found in the 9 | PATH. 10 | 11 | 1. Install the openapi-language-server binary: 12 | 13 | ```bash 14 | go install 15 | ``` 16 | 17 | 2. Run the tests: 18 | 19 | ```bash 20 | go test ./internal/e2etest -count=1 21 | ``` 22 | 23 | (Use `-count=1` to disable test caching.) 24 | 25 | ## Adding or updating tests 26 | 27 | The openapi-language-server command supports a useful flag for generating test 28 | data: 29 | 30 | ``` 31 | -testdata string 32 | Capture a copy of all input and output to the specified directory. Useful for debugging or generating test data. 33 | ``` 34 | 35 | Add this flag to your editor's language server configuration to capture test 36 | data for a specific scenario. 37 | 38 | For example, in Neovim: 39 | 40 | ```lua 41 | vim.lsp.start { 42 | cmd = { 'openapi-language-server', '-testdata', '/path/to/testdata' }, 43 | } 44 | ``` 45 | 46 | Now you can use your editor to interact with the language server and generate 47 | test data for that scenario. 48 | -------------------------------------------------------------------------------- /internal/lsp/types/lifecycle_messages.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeParams. 4 | type InitializeParams struct { 5 | ClientInfo struct { 6 | Name string `json:"name"` 7 | Version string `json:"version"` 8 | } `json:"clientInfo"` 9 | } 10 | 11 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeResult. 12 | type InitializeResult struct { 13 | Capabilities ServerCapabilities `json:"capabilities"` 14 | ServerInfo ServerInfo `json:"serverInfo"` 15 | } 16 | 17 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities. 18 | type ServerCapabilities struct { 19 | TextDocumentSync TextDocumentSyncOptions `json:"textDocumentSync"` 20 | DefinitionProvider bool `json:"definitionProvider,omitempty"` 21 | ReferencesProvider bool `json:"referencesProvider,omitempty"` 22 | } 23 | 24 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeResult. 25 | type ServerInfo struct { 26 | Name string `json:"name"` 27 | Version string `json:"version"` 28 | } 29 | -------------------------------------------------------------------------------- /internal/lsp/types/base_protocol.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | ) 7 | 8 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage. 9 | type RequestMessage struct { 10 | JSONRPC string `json:"jsonrpc"` 11 | ID *RequestID `json:"id"` 12 | Method string `json:"method"` 13 | Params json.RawMessage `json:"params"` 14 | } 15 | 16 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage. 17 | type RequestID struct { 18 | IntVal int 19 | StringVal string 20 | } 21 | 22 | func (r *RequestID) UnmarshalJSON(data []byte) error { 23 | if len(data) == 0 { 24 | return nil 25 | } 26 | 27 | if data[0] == '"' { 28 | return json.Unmarshal(data, &r.StringVal) 29 | } 30 | 31 | return json.Unmarshal(data, &r.IntVal) 32 | } 33 | 34 | func (r RequestID) MarshalJSON() ([]byte, error) { 35 | if r.StringVal != "" { 36 | return json.Marshal(r.StringVal) 37 | } 38 | 39 | return json.Marshal(r.IntVal) 40 | } 41 | 42 | func (r *RequestID) String() string { 43 | if r == nil { 44 | return "" 45 | } 46 | 47 | if r.StringVal != "" { 48 | return r.StringVal 49 | } 50 | 51 | return strconv.Itoa(r.IntVal) 52 | } 53 | 54 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage. 55 | type ResponseMessage struct { 56 | JSONRPC string `json:"jsonrpc"` 57 | ID *RequestID `json:"id"` 58 | Result any `json:"result"` 59 | } 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.2.2](https://github.com/armsnyder/openapi-language-server/compare/v0.2.1...v0.2.2) (2024-09-20) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Increase max file size from 64 KiB to 10 MiB ([c1ddaaa](https://github.com/armsnyder/openapi-language-server/commit/c1ddaaa5951a4b0a9a0124b2243b35b6e124f8e6)) 9 | 10 | ## [0.2.1](https://github.com/armsnyder/openapi-language-server/compare/v0.2.0...v0.2.1) (2024-06-24) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * fix a bug where the internal document model was not updating after file changes ([54d51dc](https://github.com/armsnyder/openapi-language-server/commit/54d51dce5c4322871f55d2751a0cf6d6c51b04a8)) 16 | 17 | ## [0.2.0](https://github.com/armsnyder/openapi-language-server/compare/v0.1.1...v0.2.0) (2024-06-24) 18 | 19 | 20 | ### ⚠ BREAKING CHANGES 21 | 22 | * rename project to openapi-language-server 23 | 24 | ### Features 25 | 26 | * rename project to openapi-language-server ([719953a](https://github.com/armsnyder/openapi-language-server/commit/719953a15c301b8dc6d41cf814188f378a9f8d68)) 27 | 28 | ## [0.1.1](https://github.com/armsnyder/openapi-language-server/compare/v0.1.0...v0.1.1) (2024-06-24) 29 | 30 | ### Bug Fixes 31 | 32 | - suppress errors when document is not found ([cd8ba00](https://github.com/armsnyder/openapi-language-server/commit/cd8ba00436c277839af95b0a4aeb3ae90b126b9c)) 33 | 34 | ## 0.1.0 (2024-06-23) 35 | 36 | ### Features 37 | 38 | - go to definition and find references for document refs ([2f0afee](https://github.com/armsnyder/openapi-language-server/commit/2f0afee71a9cd8cf1f7bd62191be6771d7d5182c)) 39 | -------------------------------------------------------------------------------- /internal/lsp/types/basic_json_structures.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "strconv" 4 | 5 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position. 6 | type Position struct { 7 | Line int `json:"line"` 8 | Character int `json:"character"` 9 | } 10 | 11 | func (p Position) String() string { 12 | return strconv.Itoa(p.Line) + ":" + strconv.Itoa(p.Character) 13 | } 14 | 15 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range. 16 | type Range struct { 17 | Start Position `json:"start"` 18 | End Position `json:"end"` 19 | } 20 | 21 | func (r Range) String() string { 22 | return r.Start.String() + "-" + r.End.String() 23 | } 24 | 25 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem. 26 | type TextDocumentItem struct { 27 | URI string `json:"uri"` 28 | Text string `json:"text"` 29 | } 30 | 31 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentIdentifier. 32 | type TextDocumentIdentifier struct { 33 | URI string `json:"uri"` 34 | } 35 | 36 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentPositionParams. 37 | type TextDocumentPositionParams struct { 38 | TextDocument TextDocumentIdentifier `json:"textDocument"` 39 | Position Position `json:"position"` 40 | } 41 | 42 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#location. 43 | type Location struct { 44 | URI string `json:"uri"` 45 | Range Range `json:"range"` 46 | } 47 | -------------------------------------------------------------------------------- /internal/lsp/handler.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import "github.com/armsnyder/openapi-language-server/internal/lsp/types" 4 | 5 | // Handler is an interface for handling LSP requests. 6 | type Handler interface { 7 | Capabilities() types.ServerCapabilities 8 | HandleOpen(params types.DidOpenTextDocumentParams) error 9 | HandleClose(params types.DidCloseTextDocumentParams) error 10 | HandleChange(params types.DidChangeTextDocumentParams) error 11 | HandleDefinition(params types.DefinitionParams) ([]types.Location, error) 12 | HandleReferences(params types.ReferenceParams) ([]types.Location, error) 13 | } 14 | 15 | // NopHandler can be embedded in a struct to provide no-op implementations of 16 | // ununsed Handler methods. 17 | type NopHandler struct{} 18 | 19 | // Capabilities implements Handler. 20 | func (NopHandler) Capabilities() types.ServerCapabilities { 21 | return types.ServerCapabilities{} 22 | } 23 | 24 | // HandleOpen implements Handler. 25 | func (NopHandler) HandleOpen(types.DidOpenTextDocumentParams) error { 26 | return nil 27 | } 28 | 29 | // HandleClose implements Handler. 30 | func (NopHandler) HandleClose(types.DidCloseTextDocumentParams) error { 31 | return nil 32 | } 33 | 34 | // HandleChange implements Handler. 35 | func (NopHandler) HandleChange(types.DidChangeTextDocumentParams) error { 36 | return nil 37 | } 38 | 39 | // HandleDefinition implements Handler. 40 | func (NopHandler) HandleDefinition(types.DefinitionParams) ([]types.Location, error) { 41 | return []types.Location{}, nil 42 | } 43 | 44 | // HandleReferences implements Handler. 45 | func (NopHandler) HandleReferences(types.ReferenceParams) ([]types.Location, error) { 46 | return []types.Location{}, nil 47 | } 48 | 49 | var _ Handler = NopHandler{} 50 | -------------------------------------------------------------------------------- /internal/lsp/types/document_synchronization.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentSyncKind. 4 | type TextDocumentSyncKind int 5 | 6 | const ( 7 | SyncNone TextDocumentSyncKind = 0 8 | SyncFull TextDocumentSyncKind = 1 9 | SyncIncremental TextDocumentSyncKind = 2 10 | ) 11 | 12 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentSyncOptions. 13 | type TextDocumentSyncOptions struct { 14 | OpenClose bool `json:"openClose,omitempty"` 15 | Change TextDocumentSyncKind `json:"change"` 16 | } 17 | 18 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#didOpenTextDocumentParams. 19 | type DidOpenTextDocumentParams struct { 20 | TextDocument TextDocumentItem `json:"textDocument"` 21 | } 22 | 23 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#didChangeTextDocumentParams. 24 | type DidChangeTextDocumentParams struct { 25 | TextDocument TextDocumentIdentifier `json:"textDocument"` 26 | ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` 27 | } 28 | 29 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent. 30 | type TextDocumentContentChangeEvent struct { 31 | Text string `json:"text"` 32 | Range *Range `json:"range,omitempty"` 33 | } 34 | 35 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#didCloseTextDocumentParams. 36 | type DidCloseTextDocumentParams struct { 37 | TextDocument TextDocumentIdentifier `json:"textDocument"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/lsp/jsonrpc/jsonrpc.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | // Split is a bufio.SplitFunc that splits JSON-RPC messages. 12 | func Split(data []byte, _ bool) (advance int, token []byte, err error) { 13 | const headerDelimiter = "\r\n\r\n" 14 | const contentLengthPrefix = "Content-Length: " 15 | 16 | header, payload, found := bytes.Cut(data, []byte(headerDelimiter)) 17 | if !found { 18 | return 0, nil, nil 19 | } 20 | 21 | contentLengthIndex := bytes.Index(header, []byte(contentLengthPrefix)) 22 | if contentLengthIndex == -1 { 23 | return 0, nil, errors.New("missing content length: header not found") 24 | } 25 | 26 | contentLengthValueStart := contentLengthIndex + len(contentLengthPrefix) 27 | contentLengthValueLength := bytes.IndexByte(header[contentLengthValueStart:], '\r') 28 | if contentLengthValueLength == -1 { 29 | contentLengthValueLength = len(header) - contentLengthValueStart 30 | } 31 | 32 | contentLength, err := strconv.Atoi(string(header[contentLengthValueStart : contentLengthValueStart+contentLengthValueLength])) 33 | if err != nil { 34 | return 0, nil, errors.New("invalid content length") 35 | } 36 | 37 | if len(payload) < contentLength { 38 | return 0, nil, nil 39 | } 40 | 41 | return len(header) + len(headerDelimiter) + contentLength, payload[:contentLength], nil 42 | } 43 | 44 | // Write writes the given JSON-encodable message using the JSON-RPC protocol. 45 | func Write(w io.Writer, message any) error { 46 | payload, err := json.Marshal(message) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return WritePayload(w, payload) 52 | } 53 | 54 | // WritePayload writes the given JSON payload using the JSON-RPC protocol. 55 | func WritePayload(w io.Writer, payload []byte) error { 56 | packet := append([]byte("Content-Length: "+strconv.Itoa(len(payload))+"\r\n\r\n"), payload...) 57 | 58 | _, err := w.Write(packet) 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Language Server 2 | 3 | An OpenAPI language server for [LSP compatible code 4 | editors.](https://microsoft.github.io/language-server-protocol/implementors/tools/) 5 | 6 | > :warning: This is beta software. Many features are still missing. See 7 | > [Features](https://github.com/armsnyder/openapi-language-server?tab=readme-ov-file#features) 8 | > below. 9 | 10 | [![asciicast](https://asciinema.org/a/v7etZb80HbYkKBQUa3dVSenPz.svg)](https://asciinema.org/a/v7etZb80HbYkKBQUa3dVSenPz) 11 | 12 | ## Features 13 | 14 | I created this language server because I manually edit OpenAPI/Swagger files, 15 | and I needed a quick way to jump between schema definitions and references. 16 | 17 | I use 18 | [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) 19 | for validation and completion, so these features are not a priority for me 20 | right now. 21 | 22 | ### Language Features 23 | 24 | - [x] Jump to definition 25 | - [x] Find references 26 | - [ ] Code completion 27 | - [ ] Diagnostics 28 | - [ ] Hover 29 | - [ ] Rename 30 | - [ ] Document symbols 31 | - [ ] Code actions 32 | 33 | ### Other Features 34 | 35 | - [x] YAML filetype support 36 | - [ ] JSON filetype support 37 | - [ ] VSCode extension 38 | 39 | ## Installation 40 | 41 | ### From GitHub Releases (Recommended) 42 | 43 | Download the latest release from [GitHub releases](https://github.com/armsnyder/openapi-language-server/releases). 44 | 45 | ### Using Go 46 | 47 | ```bash 48 | go install github.com/armsnyder/openapi-language-server@latest 49 | ``` 50 | 51 | ## Usage 52 | 53 | ### Neovim Configuration Example 54 | 55 | Assuming you are using Neovim and have the installed openapi-language-server 56 | binary in your PATH, you can use the following Lua code in your Neovim 57 | configuration: 58 | 59 | ```lua 60 | vim.api.nvim_create_autocmd('FileType', { 61 | pattern = 'yaml', 62 | callback = function() 63 | vim.lsp.start { 64 | cmd = { 'openapi-language-server' }, 65 | filetypes = { 'yaml' }, 66 | root_dir = vim.fn.getcwd(), 67 | } 68 | end, 69 | }) 70 | ``` 71 | 72 | This is just a basic working example. You will probably want to further 73 | customize the configuration to your needs. 74 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | workflow_dispatch: {} 9 | 10 | jobs: 11 | check-generated: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: "1.22" 18 | - run: go mod tidy 19 | - run: go generate ./... 20 | - run: git diff --exit-code 21 | 22 | test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-go@v5 27 | with: 28 | go-version: "1.22" 29 | - run: go install 30 | - run: go test -cover ./... 31 | 32 | lint: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-go@v5 37 | with: 38 | go-version: "1.22" 39 | - uses: golangci/golangci-lint-action@v6 40 | with: 41 | version: v1.59.1 42 | 43 | release-please: 44 | runs-on: ubuntu-latest 45 | needs: [check-generated, test, lint] 46 | if: github.ref == 'refs/heads/main' 47 | outputs: 48 | release_created: ${{ steps.release-please.outputs.release_created }} 49 | tag_name: ${{ steps.release-please.outputs.tag_name }} 50 | version: ${{ steps.release-please.outputs.version }} 51 | steps: 52 | - uses: googleapis/release-please-action@v4 53 | id: release-please 54 | with: 55 | token: ${{ secrets.PAT }} 56 | config-file: .github/release-please-config.json 57 | manifest-file: .github/.release-please-manifest.json 58 | 59 | release: 60 | runs-on: ubuntu-latest 61 | needs: [release-please] 62 | if: needs.release-please.outputs.release_created 63 | permissions: 64 | contents: write 65 | steps: 66 | - uses: actions/checkout@v4 67 | with: 68 | fetch-depth: 0 69 | - run: git fetch --tags --force 70 | - uses: actions/setup-go@v5 71 | with: 72 | go-version: "1.22" 73 | - uses: goreleaser/goreleaser-action@v6 74 | with: 75 | version: "~> v2" 76 | args: release --clean --config .github/.goreleaser.yaml 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | -------------------------------------------------------------------------------- /internal/e2etest/e2etest_test.go: -------------------------------------------------------------------------------- 1 | package e2etest_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestE2E(t *testing.T) { 16 | const testdataDir = "testdata" 17 | 18 | files, err := os.ReadDir(testdataDir) 19 | if err != nil { 20 | t.Fatalf("Failed to read testdata directory: %v", err) 21 | } 22 | 23 | for _, file := range files { 24 | if file.IsDir() { 25 | t.Run(file.Name(), func(t *testing.T) { 26 | runWithTestData(t, filepath.Join(testdataDir, file.Name())) 27 | }) 28 | } 29 | } 30 | } 31 | 32 | func runWithTestData(t *testing.T, testDataDir string) { 33 | inputData, err := os.ReadFile(testDataDir + `/input.jsonrpc`) 34 | if err != nil { 35 | t.Fatalf("Failed to read input data: %v", err) 36 | } 37 | 38 | expectedOutputData, err := os.ReadFile(testDataDir + "/output.jsonrpc") 39 | if err != nil { 40 | t.Fatalf("Failed to read expected output data: %v", err) 41 | } 42 | 43 | output := runWithInput(t, bytes.NewReader(inputData)) 44 | 45 | format := func(s string) string { 46 | return strings.ReplaceAll(strings.ReplaceAll(s, "\r", "␍"), "\n", "␊") 47 | } 48 | 49 | if string(expectedOutputData) != output { 50 | t.Errorf("Output did not match expectation.\n\ngot\n%s\n\nwant\n%s\n", format(output), format(string(expectedOutputData))) 51 | } 52 | } 53 | 54 | func runWithInput(t *testing.T, stdin io.Reader) string { 55 | var buf bytes.Buffer 56 | 57 | run(t, func(cmd *exec.Cmd) { 58 | cmd.Stdin = stdin 59 | cmd.Stdout = &buf 60 | cmd.Stderr = &testlogger{t: t, prefix: "server: "} 61 | }) 62 | 63 | return buf.String() 64 | } 65 | 66 | func run(t *testing.T, configureCommand func(cmd *exec.Cmd)) { 67 | const binName = "openapi-language-server" 68 | 69 | cmd := exec.Command(binName) 70 | 71 | t.Cleanup(func() { 72 | if cmd.Process != nil { 73 | if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) { 74 | t.Fatalf("Warning: %s process may still be running: %v", binName, err) 75 | } 76 | } 77 | }) 78 | 79 | configureCommand(cmd) 80 | 81 | if err := cmd.Start(); err != nil { 82 | var execErr *exec.Error 83 | if errors.As(err, &execErr) { 84 | if os.Getenv("CI") == "true" { 85 | t.Fatalf("End-to-end tests require %s to be installed. Error: %v", binName, err) 86 | } 87 | t.Skipf("End-to-end tests require %s to be installed. Error: %v", binName, err) 88 | } 89 | t.Fatal(err) 90 | } 91 | 92 | doneCh := make(chan error, 1) 93 | 94 | go func() { 95 | doneCh <- cmd.Wait() 96 | }() 97 | 98 | select { 99 | case err := <-doneCh: 100 | if err != nil { 101 | t.Fatalf("Server exited with error: %v", err) 102 | } 103 | case <-time.After(5 * time.Second): 104 | t.Fatal("Timeout waiting for server to exit") 105 | } 106 | } 107 | 108 | type testlogger struct { 109 | t *testing.T 110 | prefix string 111 | } 112 | 113 | // Write implements io.Writer. 114 | func (t *testlogger) Write(p []byte) (n int, err error) { 115 | t.t.Log(t.prefix + string(p)) 116 | return len(p), nil 117 | } 118 | 119 | var _ io.Writer = (*testlogger)(nil) 120 | -------------------------------------------------------------------------------- /internal/lsp/file.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "slices" 8 | "unicode/utf8" 9 | 10 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 11 | ) 12 | 13 | // File is a representation of a text file that can be modified by LSP text 14 | // document change events. It keeps track of line breaks to allow for efficient 15 | // conversion between byte offsets and LSP positions. 16 | type File struct { 17 | bytes []byte 18 | lineOffsets []int 19 | } 20 | 21 | // Bytes returns the raw bytes of the file. 22 | func (f File) Bytes() []byte { 23 | return f.bytes 24 | } 25 | 26 | // Reset initializes the file with the given content. 27 | func (f *File) Reset(s []byte) { 28 | f.bytes = s 29 | newlineCount := bytes.Count(f.bytes, []byte{'\n'}) 30 | f.lineOffsets = make([]int, 1, newlineCount+1) 31 | 32 | for i, b := range f.bytes { 33 | if b == '\n' { 34 | f.lineOffsets = append(f.lineOffsets, i+1) 35 | } 36 | } 37 | } 38 | 39 | // ApplyChange applies the given change to the file content. 40 | func (f *File) ApplyChange(change types.TextDocumentContentChangeEvent) error { 41 | if change.Range == nil { 42 | f.Reset([]byte(change.Text)) 43 | return nil 44 | } 45 | 46 | start, err := f.GetOffset(change.Range.Start) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | end, err := f.GetOffset(change.Range.End) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | f.Reset(append(f.bytes[:start], append([]byte(change.Text), f.bytes[end:]...)...)) 57 | 58 | return nil 59 | } 60 | 61 | // GetPosition returns the LSP protocol position for the given byte offset. 62 | func (f *File) GetPosition(offset int) (types.Position, error) { 63 | if offset < 0 || offset > len(f.bytes) { 64 | return types.Position{}, fmt.Errorf("offset %d is out of range [0, %d]", offset, len(f.bytes)) 65 | } 66 | 67 | line, found := slices.BinarySearch(f.lineOffsets, offset) 68 | if !found { 69 | line-- 70 | } 71 | 72 | character := UTF16Len(f.bytes[f.lineOffsets[line]:offset]) 73 | 74 | return types.Position{Line: line, Character: character}, nil 75 | } 76 | 77 | // GetOffset returns the byte offset for the given LSP protocol position. 78 | func (f *File) GetOffset(p types.Position) (int, error) { 79 | if p.Line < 0 || p.Line >= len(f.lineOffsets) { 80 | return 0, fmt.Errorf("position %s is out of range", p) 81 | } 82 | 83 | if p.Line == len(f.lineOffsets) { 84 | if p.Character == 0 { 85 | return len(f.bytes), nil 86 | } 87 | 88 | return 0, fmt.Errorf("position %s is out of range", p) 89 | } 90 | 91 | rest := f.bytes[f.lineOffsets[p.Line]:] 92 | 93 | for i := 0; i < p.Character; i++ { 94 | r, size := utf8.DecodeRune(rest) 95 | 96 | if size == 0 || r == '\n' { 97 | return 0, fmt.Errorf("position %s is out of range", p) 98 | } 99 | 100 | if r == utf8.RuneError { 101 | return 0, errors.New("invalid UTF-8 encoding") 102 | } 103 | 104 | if r >= 0x10000 { 105 | // UTF-16 surrogate pair 106 | i++ 107 | 108 | if i == p.Character { 109 | return 0, fmt.Errorf("position %s does not point to a valid UTF-16 code unit", p) 110 | } 111 | } 112 | 113 | rest = rest[size:] 114 | } 115 | 116 | return len(f.bytes) - len(rest), nil 117 | } 118 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/armsnyder/openapi-language-server/internal/analysis" 11 | "github.com/armsnyder/openapi-language-server/internal/lsp" 12 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 13 | ) 14 | 15 | // NOTE(asnyder): version is set by goreleaser using ldflags. 16 | var version = "development" 17 | 18 | func main() { 19 | // Parse command line flags. 20 | 21 | var args struct { 22 | version bool 23 | help bool 24 | testdata string 25 | } 26 | 27 | flag.BoolVar(&args.version, "version", false, "Print the version and exit") 28 | flag.BoolVar(&args.version, "v", false, "Print the version and exit") 29 | flag.BoolVar(&args.help, "help", false, "Print this help message and exit") 30 | flag.BoolVar(&args.help, "h", false, "Print this help message and exit") 31 | flag.StringVar(&args.testdata, "testdata", "", "Capture a copy of all input and output to the specified directory. Useful for debugging or generating test data.") 32 | 33 | flag.Parse() 34 | 35 | // Handle special flags. 36 | 37 | if args.version { 38 | //nolint:forbidigo // use of fmt.Println 39 | fmt.Println(version) 40 | return 41 | } 42 | 43 | if args.help { 44 | flag.Usage() 45 | return 46 | } 47 | 48 | // Configure logging. 49 | 50 | log.SetFlags(log.Lshortfile) 51 | 52 | // Configure input and output. 53 | 54 | var reader io.Reader = os.Stdin 55 | var writer io.Writer = os.Stdout 56 | 57 | if args.testdata != "" { 58 | if err := os.MkdirAll(args.testdata, 0o755); err != nil { 59 | log.Fatal("Failed to create testdata directory: ", err) 60 | } 61 | 62 | inputFile, err := os.Create(args.testdata + "/input.jsonrpc") 63 | if err != nil { 64 | log.Fatal("Failed to create input file: ", err) 65 | } 66 | defer inputFile.Close() 67 | 68 | outputFile, err := os.Create(args.testdata + "/output.jsonrpc") 69 | if err != nil { 70 | //nolint:gocritic // exitAfterDefer 71 | log.Fatal("Failed to create output file: ", err) 72 | } 73 | defer outputFile.Close() 74 | 75 | reader = io.TeeReader(reader, inputFile) 76 | writer = io.MultiWriter(writer, outputFile) 77 | } 78 | 79 | // Run the LSP server. 80 | 81 | server := &lsp.Server{ 82 | ServerInfo: types.ServerInfo{ 83 | Name: "openapi-language-server", 84 | Version: version, 85 | }, 86 | Reader: reader, 87 | Writer: writer, 88 | Handler: &analysis.Handler{}, 89 | } 90 | 91 | if err := server.Run(); err != nil { 92 | log.Fatal("LSP server error: ", err) 93 | } 94 | } 95 | 96 | // shadowReader wraps a primary reader and splits the input to a secondary 97 | // writer. 98 | type shadowReader struct { 99 | reader io.Reader 100 | shadow io.Writer 101 | } 102 | 103 | func (r shadowReader) Read(p []byte) (n int, err error) { 104 | n, err = r.reader.Read(p) 105 | _, _ = r.shadow.Write(p[:n]) 106 | return n, err 107 | } 108 | 109 | var _ io.Reader = shadowReader{} 110 | 111 | // shadowWriter wraps a primary writer and splits the output to a secondary 112 | // writer. 113 | type shadowWriter struct { 114 | writer io.Writer 115 | shadow io.Writer 116 | } 117 | 118 | func (w shadowWriter) Write(p []byte) (n int, err error) { 119 | n, err = w.writer.Write(p) 120 | _, _ = w.shadow.Write(p) 121 | return n, err 122 | } 123 | 124 | var _ io.Writer = shadowWriter{} 125 | -------------------------------------------------------------------------------- /internal/lsp/jsonrpc/jsonrpc_test.go: -------------------------------------------------------------------------------- 1 | package jsonrpc_test 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | "testing/iotest" 11 | 12 | . "github.com/armsnyder/openapi-language-server/internal/lsp/jsonrpc" 13 | ) 14 | 15 | func TestSplit(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | input string 19 | want []string 20 | }{ 21 | { 22 | name: "empty", 23 | input: "", 24 | want: nil, 25 | }, 26 | { 27 | name: "single message", 28 | input: "Content-Length: 17\r\n\r\n{\"jsonrpc\":\"2.0\"}", 29 | want: []string{ 30 | `{"jsonrpc":"2.0"}`, 31 | }, 32 | }, 33 | { 34 | name: "multiple messages", 35 | input: "Content-Length: 17\r\n\r\n{\"jsonrpc\":\"2.0\"}Content-Length: 24\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1}", 36 | want: []string{ 37 | `{"jsonrpc":"2.0"}`, 38 | `{"jsonrpc":"2.0","id":1}`, 39 | }, 40 | }, 41 | { 42 | name: "extra headers before", 43 | input: "Extra-Header: foo\r\nContent-Length: 17\r\n\r\n{\"jsonrpc\":\"2.0\"}", 44 | want: []string{ 45 | `{"jsonrpc":"2.0"}`, 46 | }, 47 | }, 48 | { 49 | name: "extra headers after", 50 | input: "Content-Length: 17\r\nExtra-Header: foo\r\n\r\n{\"jsonrpc\":\"2.0\"}", 51 | want: []string{ 52 | `{"jsonrpc":"2.0"}`, 53 | }, 54 | }, 55 | { 56 | name: "incomplete stream", 57 | input: "Content-Length: 17\r\n\r\n{\"jsonrp", 58 | want: nil, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | readers := []io.Reader{ 65 | strings.NewReader(tt.input), 66 | iotest.OneByteReader(strings.NewReader(tt.input)), 67 | iotest.HalfReader(strings.NewReader(tt.input)), 68 | } 69 | 70 | for i, r := range readers { 71 | t.Run(strconv.Itoa(i), func(t *testing.T) { 72 | scanner := bufio.NewScanner(r) 73 | scanner.Split(Split) 74 | 75 | var got []string 76 | for scanner.Scan() { 77 | got = append(got, scanner.Text()) 78 | } 79 | 80 | if err := scanner.Err(); err != nil { 81 | t.Fatal("error while scanning input: ", err) 82 | } 83 | 84 | if len(tt.want) == 0 && len(got) == 0 { 85 | return 86 | } 87 | 88 | for i := 0; i < len(tt.want) && i < len(got); i++ { 89 | if got[i] != tt.want[i] { 90 | t.Errorf("message #%d: got %q, want %q", i, got[i], tt.want[i]) 91 | } 92 | } 93 | 94 | if len(got) != len(tt.want) { 95 | t.Errorf("got %d messages, want %d", len(got), len(tt.want)) 96 | } 97 | }) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func TestWrite(t *testing.T) { 104 | tests := []struct { 105 | name string 106 | input any 107 | want string 108 | }{ 109 | { 110 | name: "null", 111 | input: nil, 112 | want: "Content-Length: 4\r\n\r\nnull", 113 | }, 114 | { 115 | name: "empty string", 116 | input: "", 117 | want: "Content-Length: 2\r\n\r\n\"\"", 118 | }, 119 | { 120 | name: "string", 121 | input: "foo", 122 | want: "Content-Length: 5\r\n\r\n\"foo\"", 123 | }, 124 | { 125 | name: "number", 126 | input: 42, 127 | want: "Content-Length: 2\r\n\r\n42", 128 | }, 129 | { 130 | name: "object", 131 | input: map[string]any{"foo": "bar"}, 132 | want: "Content-Length: 13\r\n\r\n{\"foo\":\"bar\"}", 133 | }, 134 | } 135 | 136 | for _, tt := range tests { 137 | t.Run(tt.name, func(t *testing.T) { 138 | buf := &bytes.Buffer{} 139 | if err := Write(buf, tt.input); err != nil { 140 | t.Fatal(err) 141 | } 142 | 143 | if got := buf.String(); got != tt.want { 144 | t.Errorf("got %q, want %q", got, tt.want) 145 | } 146 | }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /internal/analysis/handler.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/armsnyder/openapi-language-server/internal/analysis/yaml" 9 | "github.com/armsnyder/openapi-language-server/internal/lsp" 10 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 11 | ) 12 | 13 | // Handler implements the LSP handler for the OpenAPI Language Server. It 14 | // contains the business logic for the server. 15 | type Handler struct { 16 | lsp.NopHandler 17 | 18 | files map[string]*annotatedFile 19 | } 20 | 21 | type annotatedFile struct { 22 | file lsp.File 23 | document yaml.Document 24 | } 25 | 26 | func (h *Handler) getDocument(uri string) (yaml.Document, error) { 27 | f := h.files[uri] 28 | if f == nil { 29 | return yaml.Document{}, fmt.Errorf("unknown file: %s", uri) 30 | } 31 | 32 | if f.document.Lines == nil { 33 | document, err := yaml.Parse(bytes.NewReader(f.file.Bytes())) 34 | if err != nil { 35 | return yaml.Document{}, err 36 | } 37 | f.document = document 38 | } 39 | 40 | return f.document, nil 41 | } 42 | 43 | func (*Handler) Capabilities() types.ServerCapabilities { 44 | return types.ServerCapabilities{ 45 | TextDocumentSync: types.TextDocumentSyncOptions{ 46 | OpenClose: true, 47 | Change: types.SyncIncremental, 48 | }, 49 | DefinitionProvider: true, 50 | ReferencesProvider: true, 51 | } 52 | } 53 | 54 | func (h *Handler) HandleOpen(params types.DidOpenTextDocumentParams) error { 55 | if h.files == nil { 56 | h.files = make(map[string]*annotatedFile) 57 | } 58 | 59 | var f annotatedFile 60 | 61 | f.file.Reset([]byte(params.TextDocument.Text)) 62 | h.files[params.TextDocument.URI] = &f 63 | 64 | return nil 65 | } 66 | 67 | func (h *Handler) HandleClose(params types.DidCloseTextDocumentParams) error { 68 | delete(h.files, params.TextDocument.URI) 69 | return nil 70 | } 71 | 72 | func (h *Handler) HandleChange(params types.DidChangeTextDocumentParams) error { 73 | f, ok := h.files[params.TextDocument.URI] 74 | if !ok { 75 | log.Printf("HandleChange: Unknown file %q", params.TextDocument.URI) 76 | return nil 77 | } 78 | 79 | for _, change := range params.ContentChanges { 80 | if err := f.file.ApplyChange(change); err != nil { 81 | return err 82 | } 83 | } 84 | 85 | f.document = yaml.Document{} 86 | 87 | return nil 88 | } 89 | 90 | func (h *Handler) HandleDefinition(params types.DefinitionParams) ([]types.Location, error) { 91 | document, err := h.getDocument(params.TextDocument.URI) 92 | if err != nil { 93 | log.Printf("HandleDefinition: Error getting document %q: %v", params.TextDocument.URI, err) 94 | return nil, nil 95 | } 96 | 97 | if params.Position.Line >= len(document.Lines) { 98 | return nil, nil 99 | } 100 | 101 | ref := document.Lines[params.Position.Line].Value 102 | 103 | referencedLine := document.Locate(ref) 104 | if referencedLine == nil { 105 | return nil, nil 106 | } 107 | 108 | return []types.Location{{ 109 | URI: params.TextDocument.URI, 110 | Range: referencedLine.KeyRange, 111 | }}, nil 112 | } 113 | 114 | func (h *Handler) HandleReferences(params types.ReferenceParams) ([]types.Location, error) { 115 | document, err := h.getDocument(params.TextDocument.URI) 116 | if err != nil { 117 | log.Printf("HandleReferences: Error getting document %q: %v", params.TextDocument.URI, err) 118 | return nil, nil 119 | } 120 | 121 | if params.Position.Line >= len(document.Lines) { 122 | return nil, nil 123 | } 124 | 125 | ref := document.Lines[params.Position.Line].KeyRef() 126 | 127 | var locations []types.Location 128 | 129 | for _, line := range document.Lines { 130 | if line.Value == ref { 131 | locations = append(locations, types.Location{ 132 | URI: params.TextDocument.URI, 133 | Range: line.ValueRange, 134 | }) 135 | } 136 | } 137 | return locations, nil 138 | } 139 | 140 | var _ lsp.Handler = (*Handler)(nil) 141 | -------------------------------------------------------------------------------- /internal/e2etest/testdata/minimal/input.jsonrpc: -------------------------------------------------------------------------------- 1 | Content-Length: 4167 2 | 3 | {"jsonrpc":"2.0","method":"initialize","params":{"trace":"off","rootUri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server","rootPath":"\/Users\/adam\/repos\/armsnyder\/openapi-language-server","clientInfo":{"version":"0.10.0-dev","name":"Neovim"},"processId":63317,"workDoneToken":"1","workspaceFolders":[{"name":"\/Users\/adam\/repos\/armsnyder\/openapi-language-server","uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server"}],"capabilities":{"textDocument":{"synchronization":{"didSave":true,"willSaveWaitUntil":true,"willSave":true,"dynamicRegistration":false},"formatting":{"dynamicRegistration":true},"diagnostic":{"dynamicRegistration":false},"typeDefinition":{"linkSupport":true},"implementation":{"linkSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"inlayHint":{"resolveSupport":{"properties":["textEdits","tooltip","location","command"]},"dynamicRegistration":true},"semanticTokens":{"requests":{"range":false,"full":{"delta":true}},"dynamicRegistration":false,"formats":["relative"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"augmentsSyntaxTokens":true,"serverCancelSupport":false,"multilineTokenSupport":false,"overlappingTokenSupport":true,"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"]},"callHierarchy":{"dynamicRegistration":false},"publishDiagnostics":{"dataSupport":true,"relatedInformation":true,"tagSupport":{"valueSet":[1,2]}},"completion":{"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode","data"]},"dynamicRegistration":false,"insertTextMode":1,"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]},"contextSupport":true,"completionItem":{"labelDetailsSupport":true,"documentationFormat":["markdown","plaintext"],"insertReplaceSupport":true,"preselectSupport":true,"commitCharactersSupport":true,"snippetSupport":true,"deprecatedSupport":true,"tagSupport":{"valueSet":[1]},"insertTextModeSupport":{"valueSet":[1,2]},"resolveSupport":{"properties":["documentation","detail","additionalTextEdits","sortText","filterText","insertText","textEdit","insertTextFormat","insertTextMode"]}}},"hover":{"contentFormat":["markdown","plaintext"],"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"codeAction":{"dynamicRegistration":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"dataSupport":true,"isPreferredSupport":true,"resolveSupport":{"properties":["edit"]}},"declaration":{"linkSupport":true},"documentHighlight":{"dynamicRegistration":false},"references":{"dynamicRegistration":false},"signatureHelp":{"signatureInformation":{"activeParameterSupport":true,"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true}},"dynamicRegistration":false},"rename":{"prepareSupport":true,"dynamicRegistration":true},"documentSymbol":{"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"dynamicRegistration":false}},"general":{"positionEncodings":["utf-16"]},"window":{"workDoneProgress":true,"showMessage":{"messageActionItem":{"additionalPropertiesSupport":false}},"showDocument":{"support":true}},"workspace":{"didChangeWatchedFiles":{"relativePatternSupport":true,"dynamicRegistration":true},"symbol":{"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"dynamicRegistration":false},"workspaceEdit":{"resourceOperations":["rename","create","delete"]},"didChangeConfiguration":{"dynamicRegistration":false},"inlayHint":{"refreshSupport":true},"semanticTokens":{"refreshSupport":true},"applyEdit":true,"workspaceFolders":true,"configuration":true}}},"id":1}Content-Length: 52 4 | 5 | {"jsonrpc":"2.0","method":"initialized","params":{}}Content-Length: 249 6 | 7 | {"jsonrpc":"2.0","method":"textDocument\/didOpen","params":{"textDocument":{"version":0,"uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server\/internal\/e2etest\/testdata\/minimal\/petstore.yaml","text":"\n","languageId":"yaml"}}}Content-Length: 44 8 | 9 | {"jsonrpc":"2.0","method":"shutdown","id":2} -------------------------------------------------------------------------------- /internal/analysis/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "strings" 8 | 9 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 10 | ) 11 | 12 | // Document represents a YAML document. 13 | type Document struct { 14 | Lines []*Line 15 | Root map[string]*Line 16 | } 17 | 18 | // Locate finds a line in the document by its JSON reference URI. 19 | func (s Document) Locate(ref string) *Line { 20 | split := strings.Split(ref, "/") 21 | if len(split) < 2 { 22 | return nil 23 | } 24 | 25 | cur := s.Root[split[1]] 26 | if cur == nil { 27 | return nil 28 | } 29 | 30 | for _, key := range split[2:] { 31 | cur = cur.Children[key] 32 | if cur == nil { 33 | return nil 34 | } 35 | } 36 | 37 | return cur 38 | } 39 | 40 | // Line represents a line in a YAML document. 41 | type Line struct { 42 | Parent *Line 43 | Children map[string]*Line 44 | Key string 45 | Value string 46 | KeyRange types.Range 47 | ValueRange types.Range 48 | } 49 | 50 | // KeyRef returns the JSON reference URI that describes the key on this line. 51 | func (e *Line) KeyRef() string { 52 | keys := []string{} 53 | 54 | for cur := e; cur != nil; cur = cur.Parent { 55 | keys = append(keys, cur.Key) 56 | } 57 | 58 | var b strings.Builder 59 | 60 | b.WriteString("#/") 61 | 62 | for i := len(keys) - 1; i >= 0; i-- { 63 | b.WriteString(keys[i]) 64 | 65 | if i > 0 { 66 | b.WriteByte('/') 67 | } 68 | } 69 | 70 | return b.String() 71 | } 72 | 73 | type lineWithIndent struct { 74 | line *Line 75 | indent int 76 | } 77 | 78 | // Parse parses a YAML document from a reader, using best-effort. The YAML does 79 | // not need to be syntactically valid. 80 | func Parse(r io.Reader) (Document, error) { 81 | parentStack := []lineWithIndent{} 82 | scanner := bufio.NewScanner(r) 83 | document := Document{ 84 | Root: map[string]*Line{}, 85 | } 86 | 87 | for lineNum := 0; scanner.Scan(); lineNum++ { 88 | line := parseLine(scanner.Bytes(), lineNum) 89 | document.Lines = append(document.Lines, line.line) 90 | 91 | for len(parentStack) > 0 && parentStack[len(parentStack)-1].indent >= line.indent { 92 | parentStack = parentStack[:len(parentStack)-1] 93 | } 94 | 95 | if len(parentStack) == 0 { 96 | document.Root[line.line.Key] = line.line 97 | parentStack = append(parentStack, line) 98 | continue 99 | } 100 | 101 | parent := parentStack[len(parentStack)-1] 102 | line.line.Parent = parent.line 103 | if parent.line.Children == nil { 104 | parent.line.Children = map[string]*Line{} 105 | } 106 | parent.line.Children[line.line.Key] = line.line 107 | parentStack = append(parentStack, line) 108 | } 109 | 110 | if err := scanner.Err(); err != nil { 111 | return Document{}, err 112 | } 113 | 114 | return document, nil 115 | } 116 | 117 | func parseLine(s []byte, lineNum int) lineWithIndent { 118 | result := lineWithIndent{ 119 | line: &Line{}, 120 | } 121 | 122 | result.indent = bytes.IndexFunc(s, func(ch rune) bool { 123 | return ch != ' ' 124 | }) 125 | if result.indent == -1 { 126 | result.indent = len(s) 127 | } 128 | 129 | keyEnd := bytes.Index(s, []byte(":")) 130 | if keyEnd == -1 { 131 | return result 132 | } 133 | 134 | result.line.Key = string(s[result.indent:keyEnd]) 135 | result.line.KeyRange = types.Range{ 136 | Start: types.Position{Line: lineNum, Character: result.indent}, 137 | End: types.Position{Line: lineNum, Character: keyEnd}, 138 | } 139 | 140 | valueStart := bytes.IndexFunc(s[keyEnd+1:], func(ch rune) bool { 141 | return ch != ' ' 142 | }) 143 | if valueStart == -1 { 144 | return result 145 | } 146 | valueStart += keyEnd + 1 147 | if valueStart >= len(s) { 148 | return result 149 | } 150 | 151 | if s[valueStart] == '"' || s[valueStart] == '\'' { 152 | valueEnd := bytes.LastIndex(s, s[valueStart:valueStart+1]) 153 | if valueEnd <= valueStart { 154 | return result 155 | } 156 | 157 | result.line.Value = string(s[valueStart+1 : valueEnd]) 158 | result.line.ValueRange = types.Range{ 159 | Start: types.Position{Line: lineNum, Character: valueStart + 1}, 160 | End: types.Position{Line: lineNum, Character: valueEnd}, 161 | } 162 | 163 | return result 164 | } 165 | 166 | result.line.Value = string(s[valueStart:]) 167 | result.line.ValueRange = types.Range{ 168 | Start: types.Position{Line: lineNum, Character: valueStart}, 169 | End: types.Position{Line: lineNum, Character: len(s)}, 170 | } 171 | 172 | return result 173 | } 174 | -------------------------------------------------------------------------------- /internal/lsp/testutil/handler.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/lsp/handler.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source internal/lsp/handler.go -destination internal/lsp/testutil/handler.go -package testutil 7 | // 8 | 9 | // Package testutil is a generated GoMock package. 10 | package testutil 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | types "github.com/armsnyder/openapi-language-server/internal/lsp/types" 16 | gomock "go.uber.org/mock/gomock" 17 | ) 18 | 19 | // MockHandler is a mock of Handler interface. 20 | type MockHandler struct { 21 | ctrl *gomock.Controller 22 | recorder *MockHandlerMockRecorder 23 | } 24 | 25 | // MockHandlerMockRecorder is the mock recorder for MockHandler. 26 | type MockHandlerMockRecorder struct { 27 | mock *MockHandler 28 | } 29 | 30 | // NewMockHandler creates a new mock instance. 31 | func NewMockHandler(ctrl *gomock.Controller) *MockHandler { 32 | mock := &MockHandler{ctrl: ctrl} 33 | mock.recorder = &MockHandlerMockRecorder{mock} 34 | return mock 35 | } 36 | 37 | // EXPECT returns an object that allows the caller to indicate expected use. 38 | func (m *MockHandler) EXPECT() *MockHandlerMockRecorder { 39 | return m.recorder 40 | } 41 | 42 | // Capabilities mocks base method. 43 | func (m *MockHandler) Capabilities() types.ServerCapabilities { 44 | m.ctrl.T.Helper() 45 | ret := m.ctrl.Call(m, "Capabilities") 46 | ret0, _ := ret[0].(types.ServerCapabilities) 47 | return ret0 48 | } 49 | 50 | // Capabilities indicates an expected call of Capabilities. 51 | func (mr *MockHandlerMockRecorder) Capabilities() *gomock.Call { 52 | mr.mock.ctrl.T.Helper() 53 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capabilities", reflect.TypeOf((*MockHandler)(nil).Capabilities)) 54 | } 55 | 56 | // HandleChange mocks base method. 57 | func (m *MockHandler) HandleChange(params types.DidChangeTextDocumentParams) error { 58 | m.ctrl.T.Helper() 59 | ret := m.ctrl.Call(m, "HandleChange", params) 60 | ret0, _ := ret[0].(error) 61 | return ret0 62 | } 63 | 64 | // HandleChange indicates an expected call of HandleChange. 65 | func (mr *MockHandlerMockRecorder) HandleChange(params any) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleChange", reflect.TypeOf((*MockHandler)(nil).HandleChange), params) 68 | } 69 | 70 | // HandleClose mocks base method. 71 | func (m *MockHandler) HandleClose(params types.DidCloseTextDocumentParams) error { 72 | m.ctrl.T.Helper() 73 | ret := m.ctrl.Call(m, "HandleClose", params) 74 | ret0, _ := ret[0].(error) 75 | return ret0 76 | } 77 | 78 | // HandleClose indicates an expected call of HandleClose. 79 | func (mr *MockHandlerMockRecorder) HandleClose(params any) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleClose", reflect.TypeOf((*MockHandler)(nil).HandleClose), params) 82 | } 83 | 84 | // HandleDefinition mocks base method. 85 | func (m *MockHandler) HandleDefinition(params types.DefinitionParams) ([]types.Location, error) { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "HandleDefinition", params) 88 | ret0, _ := ret[0].([]types.Location) 89 | ret1, _ := ret[1].(error) 90 | return ret0, ret1 91 | } 92 | 93 | // HandleDefinition indicates an expected call of HandleDefinition. 94 | func (mr *MockHandlerMockRecorder) HandleDefinition(params any) *gomock.Call { 95 | mr.mock.ctrl.T.Helper() 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleDefinition", reflect.TypeOf((*MockHandler)(nil).HandleDefinition), params) 97 | } 98 | 99 | // HandleOpen mocks base method. 100 | func (m *MockHandler) HandleOpen(params types.DidOpenTextDocumentParams) error { 101 | m.ctrl.T.Helper() 102 | ret := m.ctrl.Call(m, "HandleOpen", params) 103 | ret0, _ := ret[0].(error) 104 | return ret0 105 | } 106 | 107 | // HandleOpen indicates an expected call of HandleOpen. 108 | func (mr *MockHandlerMockRecorder) HandleOpen(params any) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleOpen", reflect.TypeOf((*MockHandler)(nil).HandleOpen), params) 111 | } 112 | 113 | // HandleReferences mocks base method. 114 | func (m *MockHandler) HandleReferences(params types.ReferenceParams) ([]types.Location, error) { 115 | m.ctrl.T.Helper() 116 | ret := m.ctrl.Call(m, "HandleReferences", params) 117 | ret0, _ := ret[0].([]types.Location) 118 | ret1, _ := ret[1].(error) 119 | return ret0, ret1 120 | } 121 | 122 | // HandleReferences indicates an expected call of HandleReferences. 123 | func (mr *MockHandlerMockRecorder) HandleReferences(params any) *gomock.Call { 124 | mr.mock.ctrl.T.Helper() 125 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleReferences", reflect.TypeOf((*MockHandler)(nil).HandleReferences), params) 126 | } 127 | -------------------------------------------------------------------------------- /internal/lsp/file_test.go: -------------------------------------------------------------------------------- 1 | package lsp_test 2 | 3 | import ( 4 | "regexp" 5 | "runtime" 6 | "strconv" 7 | "testing" 8 | 9 | . "github.com/armsnyder/openapi-language-server/internal/lsp" 10 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 11 | ) 12 | 13 | func TestFile(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | text string 17 | steps []Step 18 | }{ 19 | { 20 | name: "hand type from empty", 21 | text: "\n", 22 | steps: []Step{ 23 | {Text: "a", Range: "0:0-0:0", Want: "a\n"}, 24 | {Text: "b", Range: "0:1-0:1", Want: "ab\n"}, 25 | {Text: "\n", Range: "0:2-0:2", Want: "ab\n\n"}, 26 | {Text: "c", Range: "1:0-1:0", Want: "ab\nc\n"}, 27 | {Text: "d", Range: "1:1-1:1", Want: "ab\ncd\n"}, 28 | }, 29 | }, 30 | { 31 | name: "hand delete from end", 32 | text: "ab\ncd\n", 33 | steps: []Step{ 34 | {Text: "", Range: "1:1-1:2", Want: "ab\nc\n"}, 35 | {Text: "", Range: "1:0-1:1", Want: "ab\n\n"}, 36 | {Text: "", Range: "0:2-0:2", Want: "ab\n\n"}, 37 | {Text: "", Range: "1:0-2:0", Want: "ab\n"}, 38 | {Text: "", Range: "0:1-0:2", Want: "a\n"}, 39 | {Text: "", Range: "0:0-0:1", Want: "\n"}, 40 | }, 41 | }, 42 | { 43 | name: "add 2 lines to the middle then update each line", 44 | text: "ab\ncd\n", 45 | steps: []Step{ 46 | {Text: "\n12\n34", Range: "0:2-0:2", Want: "ab\n12\n34\ncd\n"}, 47 | {Text: "x", Range: "3:1-3:2", Want: "ab\n12\n34\ncx\n"}, 48 | {Text: "y", Range: "2:1-2:2", Want: "ab\n12\n3y\ncx\n"}, 49 | {Text: "z", Range: "1:1-1:2", Want: "ab\n1z\n3y\ncx\n"}, 50 | }, 51 | }, 52 | { 53 | name: "insert text at the beginning of the file", 54 | text: "line1\nline2\nline3\n", 55 | steps: []Step{ 56 | {Text: "start\n", Range: "0:0-0:0", Want: "start\nline1\nline2\nline3\n"}, 57 | }, 58 | }, 59 | { 60 | name: "insert text at the end of the file", 61 | text: "line1\nline2\nline3\n", 62 | steps: []Step{ 63 | {Text: "end\n", Range: "3:0-3:0", Want: "line1\nline2\nline3\nend\n"}, 64 | }, 65 | }, 66 | { 67 | name: "insert newline at the beginning and end of the file", 68 | text: "line1\nline2\nline3\n", 69 | steps: []Step{ 70 | {Text: "\n", Range: "0:0-0:0", Want: "\nline1\nline2\nline3\n"}, 71 | {Text: "\n", Range: "4:0-4:0", Want: "\nline1\nline2\nline3\n\n"}, 72 | }, 73 | }, 74 | { 75 | name: "delete text spanning multiple lines", 76 | text: "line1\nline2\nline3\nline4\n", 77 | steps: []Step{ 78 | {Text: "", Range: "1:2-3:4", Want: "line1\nli4\n"}, 79 | {Text: "x", Range: "1:3-1:3", Want: "line1\nli4x\n"}, 80 | }, 81 | }, 82 | { 83 | name: "replace text spanning multiple lines with text containing newlines", 84 | text: "line1\nline2\nline3\nline4\n", 85 | steps: []Step{ 86 | {Text: "new\ntext\n", Range: "1:2-3:4", Want: "line1\nlinew\ntext\n4\n"}, 87 | {Text: "x", Range: "3:1-3:1", Want: "line1\nlinew\ntext\n4x\n"}, 88 | }, 89 | }, 90 | { 91 | name: "delete final line", 92 | text: "a\n", 93 | steps: []Step{ 94 | {Text: "", Range: "0:0-1:0", Want: ""}, 95 | }, 96 | }, 97 | { 98 | name: "add to empty file without newline", 99 | text: "", 100 | steps: []Step{ 101 | {Text: "\n\n", Range: "0:0-0:0", Want: "\n\n"}, 102 | }, 103 | }, 104 | } 105 | 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | var f File 109 | f.Reset([]byte(tt.text)) 110 | for i, step := range tt.steps { 111 | func() { 112 | defer func() { 113 | if r := recover(); r != nil { 114 | stack := make([]byte, 1<<16) 115 | stack = stack[:runtime.Stack(stack, false)] 116 | t.Fatalf("step %d: %v\n%s", i, r, stack) 117 | } 118 | }() 119 | event := newEvent(step.Text, step.Range) 120 | if err := f.ApplyChange(event); err != nil { 121 | t.Fatalf("step %d: %v", i, err) 122 | } 123 | if got := string(f.Bytes()); got != step.Want { 124 | t.Fatalf("step %d: got %q, want %q", i, got, step.Want) 125 | } 126 | }() 127 | } 128 | }) 129 | } 130 | } 131 | 132 | type Step struct { 133 | Text string 134 | Range string 135 | Want string 136 | } 137 | 138 | var rangePattern = regexp.MustCompile(`^(\d+):(\d+)-(\d+):(\d+)$`) 139 | 140 | func newEvent(text, rng string) types.TextDocumentContentChangeEvent { 141 | match := rangePattern.FindSubmatch([]byte(rng)) 142 | if match == nil { 143 | panic("invalid range") 144 | } 145 | 146 | return types.TextDocumentContentChangeEvent{ 147 | Text: text, 148 | Range: &types.Range{ 149 | Start: types.Position{ 150 | Line: mustAtoi(match[1]), 151 | Character: mustAtoi(match[2]), 152 | }, 153 | End: types.Position{ 154 | Line: mustAtoi(match[3]), 155 | Character: mustAtoi(match[4]), 156 | }, 157 | }, 158 | } 159 | } 160 | 161 | func mustAtoi(b []byte) int { 162 | i, err := strconv.Atoi(string(b)) 163 | if err != nil { 164 | panic(err) 165 | } 166 | return i 167 | } 168 | -------------------------------------------------------------------------------- /internal/e2etest/testdata/definition/input.jsonrpc: -------------------------------------------------------------------------------- 1 | Content-Length: 4167 2 | 3 | {"method":"initialize","id":1,"params":{"processId":65382,"workDoneToken":"1","workspaceFolders":[{"name":"\/Users\/adam\/repos\/armsnyder\/openapi-language-server","uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server"}],"trace":"off","capabilities":{"textDocument":{"documentHighlight":{"dynamicRegistration":false},"references":{"dynamicRegistration":false},"signatureHelp":{"dynamicRegistration":false,"signatureInformation":{"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true,"documentationFormat":["markdown","plaintext"]}},"synchronization":{"dynamicRegistration":false,"willSaveWaitUntil":true,"didSave":true,"willSave":true},"diagnostic":{"dynamicRegistration":false},"typeDefinition":{"linkSupport":true},"implementation":{"linkSupport":true},"definition":{"linkSupport":true,"dynamicRegistration":true},"completion":{"dynamicRegistration":false,"insertTextMode":1,"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]},"contextSupport":true,"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode","data"]},"completionItem":{"resolveSupport":{"properties":["documentation","detail","additionalTextEdits","sortText","filterText","insertText","textEdit","insertTextFormat","insertTextMode"]},"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"commitCharactersSupport":true,"snippetSupport":true,"insertReplaceSupport":true,"tagSupport":{"valueSet":[1]},"insertTextModeSupport":{"valueSet":[1,2]},"labelDetailsSupport":true}},"inlayHint":{"dynamicRegistration":true,"resolveSupport":{"properties":["textEdits","tooltip","location","command"]}},"semanticTokens":{"augmentsSyntaxTokens":true,"serverCancelSupport":false,"multilineTokenSupport":false,"overlappingTokenSupport":true,"requests":{"range":false,"full":{"delta":true}},"dynamicRegistration":false,"formats":["relative"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"]},"callHierarchy":{"dynamicRegistration":false},"publishDiagnostics":{"tagSupport":{"valueSet":[1,2]},"dataSupport":true,"relatedInformation":true},"declaration":{"linkSupport":true},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":false,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true},"codeAction":{"dynamicRegistration":true,"dataSupport":true,"isPreferredSupport":true,"resolveSupport":{"properties":["edit"]},"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"rename":{"dynamicRegistration":true,"prepareSupport":true}},"workspace":{"semanticTokens":{"refreshSupport":true},"applyEdit":true,"workspaceFolders":true,"configuration":true,"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true},"symbol":{"dynamicRegistration":false,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"workspaceEdit":{"resourceOperations":["rename","create","delete"]},"didChangeConfiguration":{"dynamicRegistration":false},"inlayHint":{"refreshSupport":true}},"general":{"positionEncodings":["utf-16"]},"window":{"showDocument":{"support":true},"workDoneProgress":true,"showMessage":{"messageActionItem":{"additionalPropertiesSupport":false}}}},"rootUri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server","rootPath":"\/Users\/adam\/repos\/armsnyder\/openapi-language-server","clientInfo":{"version":"0.10.0-dev","name":"Neovim"}},"jsonrpc":"2.0"}Content-Length: 52 4 | 5 | {"method":"initialized","params":{},"jsonrpc":"2.0"}Content-Length: 476 6 | 7 | {"method":"textDocument\/didOpen","params":{"textDocument":{"text":"paths:\n \/pet:\n post:\n requestBody:\n content:\n application\/json:\n schema:\n $ref: \"#\/components\/schemas\/Pet\"\ncomponents:\n schemas:\n Pet:\n type: object\n","uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server\/internal\/e2etest\/testdata\/definition\/petstore.yaml","version":0,"languageId":"yaml"}},"jsonrpc":"2.0"}Content-Length: 255 8 | 9 | {"method":"textDocument\/definition","id":2,"params":{"textDocument":{"uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server\/internal\/e2etest\/testdata\/definition\/petstore.yaml"},"position":{"character":20,"line":7}},"jsonrpc":"2.0"}Content-Length: 255 10 | 11 | {"method":"textDocument\/definition","id":3,"params":{"textDocument":{"uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server\/internal\/e2etest\/testdata\/definition\/petstore.yaml"},"position":{"character":11,"line":4}},"jsonrpc":"2.0"}Content-Length: 44 12 | 13 | {"method":"shutdown","id":4,"jsonrpc":"2.0"} -------------------------------------------------------------------------------- /internal/e2etest/testdata/references/input.jsonrpc: -------------------------------------------------------------------------------- 1 | Content-Length: 4167 2 | 3 | {"id":1,"params":{"rootUri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server","rootPath":"\/Users\/adam\/repos\/armsnyder\/openapi-language-server","clientInfo":{"version":"0.10.0-dev","name":"Neovim"},"processId":67413,"workDoneToken":"1","capabilities":{"general":{"positionEncodings":["utf-16"]},"textDocument":{"declaration":{"linkSupport":true},"callHierarchy":{"dynamicRegistration":false},"publishDiagnostics":{"tagSupport":{"valueSet":[1,2]},"dataSupport":true,"relatedInformation":true},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"rangeFormatting":{"dynamicRegistration":true},"codeAction":{"resolveSupport":{"properties":["edit"]},"dynamicRegistration":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"dataSupport":true,"isPreferredSupport":true},"documentHighlight":{"dynamicRegistration":false},"diagnostic":{"dynamicRegistration":false},"signatureHelp":{"dynamicRegistration":false,"signatureInformation":{"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true,"documentationFormat":["markdown","plaintext"]}},"formatting":{"dynamicRegistration":true},"synchronization":{"dynamicRegistration":false,"willSave":true,"didSave":true,"willSaveWaitUntil":true},"typeDefinition":{"linkSupport":true},"completion":{"contextSupport":true,"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode","data"]},"dynamicRegistration":false,"insertTextMode":1,"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]},"completionItem":{"resolveSupport":{"properties":["documentation","detail","additionalTextEdits","sortText","filterText","insertText","textEdit","insertTextFormat","insertTextMode"]},"insertTextModeSupport":{"valueSet":[1,2]},"insertReplaceSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"commitCharactersSupport":true,"snippetSupport":true,"tagSupport":{"valueSet":[1]},"labelDetailsSupport":true}},"definition":{"linkSupport":true,"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true},"inlayHint":{"dynamicRegistration":true,"resolveSupport":{"properties":["textEdits","tooltip","location","command"]}},"semanticTokens":{"requests":{"full":{"delta":true},"range":false},"dynamicRegistration":false,"formats":["relative"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"],"augmentsSyntaxTokens":true,"serverCancelSupport":false,"multilineTokenSupport":false,"overlappingTokenSupport":true},"implementation":{"linkSupport":true},"references":{"dynamicRegistration":false},"documentSymbol":{"dynamicRegistration":false,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true}},"workspace":{"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true},"workspaceEdit":{"resourceOperations":["rename","create","delete"]},"symbol":{"dynamicRegistration":false,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"didChangeConfiguration":{"dynamicRegistration":false},"inlayHint":{"refreshSupport":true},"semanticTokens":{"refreshSupport":true},"applyEdit":true,"workspaceFolders":true,"configuration":true},"window":{"showDocument":{"support":true},"workDoneProgress":true,"showMessage":{"messageActionItem":{"additionalPropertiesSupport":false}}}},"trace":"off","workspaceFolders":[{"name":"\/Users\/adam\/repos\/armsnyder\/openapi-language-server","uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server"}]},"jsonrpc":"2.0","method":"initialize"}Content-Length: 52 4 | 5 | {"params":{},"jsonrpc":"2.0","method":"initialized"}Content-Length: 579 6 | 7 | {"params":{"textDocument":{"uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server\/internal\/e2etest\/testdata\/references\/petstore.yaml","languageId":"yaml","text":"paths:\n \/pet:\n post:\n requestBody:\n content:\n application\/json:\n schema:\n $ref: \"#\/components\/schemas\/Pet\"\n application\/xml:\n schema:\n $ref: \"#\/components\/schemas\/Pet\"\ncomponents:\n schemas:\n Pet:\n type: object\n","version":0}},"jsonrpc":"2.0","method":"textDocument\/didOpen"}Content-Length: 293 8 | 9 | {"id":2,"params":{"position":{"line":13,"character":4},"textDocument":{"uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server\/internal\/e2etest\/testdata\/references\/petstore.yaml"},"context":{"includeDeclaration":true}},"jsonrpc":"2.0","method":"textDocument\/references"}Content-Length: 293 10 | 11 | {"id":3,"params":{"position":{"line":12,"character":4},"textDocument":{"uri":"file:\/\/\/Users\/adam\/repos\/armsnyder\/openapi-language-server\/internal\/e2etest\/testdata\/references\/petstore.yaml"},"context":{"includeDeclaration":true}},"jsonrpc":"2.0","method":"textDocument\/references"}Content-Length: 44 12 | 13 | {"id":4,"jsonrpc":"2.0","method":"shutdown"} -------------------------------------------------------------------------------- /internal/lsp/server.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | 11 | "github.com/armsnyder/openapi-language-server/internal/lsp/jsonrpc" 12 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 13 | ) 14 | 15 | // Server is an LSP server. It handles the I/O and delegates handling of 16 | // requests to a Handler. 17 | type Server struct { 18 | Reader io.Reader 19 | Writer io.Writer 20 | Handler Handler 21 | ServerInfo types.ServerInfo 22 | } 23 | 24 | // Run is a blocking function that reads from the server's Reader, processes 25 | // requests, and writes responses to the server's Writer. It returns an error 26 | // if the server stops unexpectedly. 27 | func (s *Server) Run() error { 28 | scanner := bufio.NewScanner(s.Reader) 29 | scanner.Buffer(nil, 10*1024*1024) 30 | scanner.Split(jsonrpc.Split) 31 | 32 | log.Println("LSP server started") 33 | 34 | for scanner.Scan() { 35 | if err := s.handleRequestPayload(scanner.Bytes()); err != nil { 36 | if errors.Is(err, errShutdown) { 37 | log.Println("LSP server shutting down") 38 | return nil 39 | } 40 | 41 | return err 42 | } 43 | } 44 | 45 | return scanner.Err() 46 | } 47 | 48 | func (s *Server) handleRequestPayload(payload []byte) (err error) { 49 | var request types.RequestMessage 50 | 51 | err = json.Unmarshal(payload, &request) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if request.JSONRPC != "2.0" { 57 | return errors.New("unknown jsonrpc version") 58 | } 59 | 60 | if request.Method == "" { 61 | return errors.New("request is missing a method") 62 | } 63 | 64 | return s.handleRequest(request) 65 | } 66 | 67 | var errShutdown = errors.New("shutdown") 68 | 69 | func (s *Server) handleRequest(request types.RequestMessage) error { 70 | switch request.Method { 71 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize 72 | case "initialize": 73 | var params types.InitializeParams 74 | if err := json.Unmarshal(request.Params, ¶ms); err != nil { 75 | return fmt.Errorf("invalid initialize params: %w", err) 76 | } 77 | 78 | log.Printf("Connected to: %s %s", params.ClientInfo.Name, params.ClientInfo.Version) 79 | 80 | s.write(request, types.InitializeResult{ 81 | Capabilities: s.Handler.Capabilities(), 82 | ServerInfo: s.ServerInfo, 83 | }) 84 | 85 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialized 86 | case "initialized": 87 | // No-op 88 | 89 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#shutdown 90 | case "shutdown": 91 | s.write(request, nil) 92 | return errShutdown 93 | 94 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen 95 | case "textDocument/didOpen": 96 | var params types.DidOpenTextDocumentParams 97 | if err := json.Unmarshal(request.Params, ¶ms); err != nil { 98 | return fmt.Errorf("invalid textDocument/didOpen params: %w", err) 99 | } 100 | 101 | if err := s.Handler.HandleOpen(params); err != nil { 102 | return err 103 | } 104 | 105 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose 106 | case "textDocument/didClose": 107 | var params types.DidCloseTextDocumentParams 108 | if err := json.Unmarshal(request.Params, ¶ms); err != nil { 109 | return fmt.Errorf("invalid textDocument/didClose params: %w", err) 110 | } 111 | 112 | if err := s.Handler.HandleClose(params); err != nil { 113 | return err 114 | } 115 | 116 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange 117 | case "textDocument/didChange": 118 | var params types.DidChangeTextDocumentParams 119 | if err := json.Unmarshal(request.Params, ¶ms); err != nil { 120 | return fmt.Errorf("invalid textDocument/didChange params: %w", err) 121 | } 122 | 123 | if err := s.Handler.HandleChange(params); err != nil { 124 | return err 125 | } 126 | 127 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition 128 | case "textDocument/definition": 129 | var params types.DefinitionParams 130 | if err := json.Unmarshal(request.Params, ¶ms); err != nil { 131 | return fmt.Errorf("invalid textDocument/definition params: %w", err) 132 | } 133 | 134 | location, err := s.Handler.HandleDefinition(params) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | s.write(request, location) 140 | 141 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references 142 | case "textDocument/references": 143 | var params types.ReferenceParams 144 | if err := json.Unmarshal(request.Params, ¶ms); err != nil { 145 | return fmt.Errorf("invalid textDocument/references params: %w", err) 146 | } 147 | 148 | locations, err := s.Handler.HandleReferences(params) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | s.write(request, locations) 154 | 155 | default: 156 | log.Printf("Warning: Request with unknown method %q", request.Method) 157 | } 158 | 159 | return nil 160 | } 161 | 162 | func (s *Server) write(request types.RequestMessage, result any) { 163 | if err := jsonrpc.Write(s.Writer, types.ResponseMessage{ 164 | JSONRPC: "2.0", 165 | ID: request.ID, 166 | Result: result, 167 | }); err != nil { 168 | log.Printf("Error writing response: %v", err) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # See https://golangci-lint.run/usage/configuration/ 2 | 3 | linters: 4 | disable-all: true 5 | enable: 6 | # See https://golangci-lint.run/usage/linters/ 7 | - asasalint # Check for pass []any as any in variadic func(...any). 8 | - bodyclose # Checks whether HTTP response body is closed successfully. 9 | - contextcheck # Check whether the function uses a non-inherited context. 10 | - durationcheck # Check for two durations multiplied together. 11 | - errcheck # Checks whether Rows.Err of rows is checked successfully. 12 | - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted. 13 | - errorlint # Errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. 14 | - forbidigo # Forbids identifiers. 15 | - gci # Gci controls Go package import order and makes it always deterministic. 16 | - gocritic # Provides diagnostics that check for bugs, performance and style issues. Extensible without recompilation through dynamic rules. Dynamic rules are written declaratively with AST patterns, filters, report message and optional suggestion. 17 | - godot # Check if comments end in a period. 18 | - gosec # Inspects source code for security problems. 19 | - gosimple # Linter for Go source code that specializes in simplifying code. 20 | - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. 21 | - inamedparam # Reports interfaces with unnamed method parameters. 22 | - ineffassign # Detects when assignments to existing variables are not used. 23 | - mirror # Reports wrong mirror patterns of bytes/strings usage. 24 | - misspell # Finds commonly misspelled English words. 25 | - musttag # Enforce field tags in (un)marshaled structs. 26 | - nilerr # Finds the code that returns nil even if it checks that the error is not nil. 27 | - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. 28 | - noctx # Finds sending http request without context.Context. 29 | - nolintlint # Reports ill-formed or insufficient nolint directives. 30 | - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. 31 | - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. 32 | - protogetter # Reports direct reads from proto message fields when getters should be used. 33 | - reassign # Checks that package variables are not reassigned. 34 | - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. 35 | - staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. 36 | - tenv # # Tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17. 37 | - unconvert # Remove unnecessary type conversions. 38 | - unused # Checks Go code for unused constants, variables, functions and types. 39 | 40 | linters-settings: 41 | # See https://golangci-lint.run/usage/linters/#linters-configuration 42 | forbidigo: 43 | forbid: 44 | - 'fmt\.Print.*' # Should be using a logger 45 | gci: 46 | sections: 47 | - standard 48 | - default 49 | - prefix(github.com/armsnyder) 50 | gocritic: 51 | enabled-tags: 52 | - performance 53 | - opinionated 54 | - experimental 55 | disabled-checks: 56 | - whyNoLint # False positives, use nolintlint instead 57 | govet: 58 | enable-all: true 59 | disable: 60 | - fieldalignment # Too struct 61 | nolintlint: 62 | require-specific: true 63 | revive: 64 | enable-all-rules: true 65 | rules: 66 | # See https://revive.run/r 67 | - name: add-constant # too strict 68 | disabled: true 69 | - name: argument-limit # too strict 70 | disabled: true 71 | - name: cognitive-complexity 72 | arguments: 73 | - 30 74 | - name: cyclomatic 75 | arguments: 76 | - 30 77 | - name: file-header # too strict 78 | disabled: true 79 | - name: function-length 80 | arguments: 81 | - 50 # statements 82 | - 0 # lines (0 to disable) 83 | - name: function-result-limit # too strict 84 | disabled: true 85 | - name: import-shadowing # too strict, results in uglier code 86 | disabled: true 87 | - name: line-length-limit # too strict 88 | disabled: true 89 | - name: max-public-structs # too strict 90 | disabled: true 91 | - name: modifies-parameter # too strict 92 | disabled: true 93 | - name: modifies-value-receiver # too strict 94 | disabled: true 95 | - name: nested-structs # too strict 96 | disabled: true 97 | - name: package-comments # too strict 98 | disabled: true 99 | - name: unhandled-error 100 | disabled: true # not as good as errcheck 101 | 102 | issues: 103 | exclude-rules: 104 | - path: _test\.go$ 105 | linters: 106 | - gosec # too strict 107 | - noctx # too strict 108 | - path: _test\.go$ 109 | text: (cognitive-complexity|function-length|dot-imports|import-alias-naming) # too strict 110 | linters: 111 | - revive 112 | # main.go is allowed to contain early bootstrapping print statements. 113 | # TestMain is allowed to log. 114 | - path: \/main(_test)?\.go$ 115 | text: fmt.Print 116 | linters: 117 | - forbidigo 118 | # Shadowing err is common. 119 | - text: 'shadow: declaration of "err"' 120 | linters: 121 | - govet 122 | - text: "^exported:.+stutters" # too strict and gets in the way of combining types like handlers 123 | linters: 124 | - revive 125 | - path: _test\.go$ 126 | text: "unused-parameter" # too strict 127 | -------------------------------------------------------------------------------- /internal/analysis/handler_test.go: -------------------------------------------------------------------------------- 1 | package analysis_test 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | . "github.com/armsnyder/openapi-language-server/internal/analysis" 11 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 12 | ) 13 | 14 | type HandlerSetupFunc func(t *testing.T, h *Handler) 15 | 16 | func TestHandler_HandleDefinition(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | setup HandlerSetupFunc 20 | params types.DefinitionParams 21 | want []types.Location 22 | wantErr bool 23 | }{ 24 | { 25 | name: "file not found", 26 | setup: loadFile("file:///foo", "foo"), 27 | params: definitionParams("file:///bar", "0:0"), 28 | }, 29 | { 30 | name: "no definition", 31 | setup: loadFile("file:///foo", "foo"), 32 | params: definitionParams("file:///foo", "0:0"), 33 | }, 34 | { 35 | name: "start of ref", 36 | setup: loadFile("file:///foo", ` 37 | foo: 38 | $ref: "#/bar/baz" 39 | bar: 40 | baz: 41 | type: object`), 42 | params: definitionParams("file:///foo", "2:8"), 43 | want: locations("file:///foo", "4:2-4:5"), 44 | }, 45 | { 46 | name: "end of ref", 47 | setup: loadFile("file:///foo", ` 48 | foo: 49 | $ref: "#/bar/baz" 50 | bar: 51 | baz: 52 | type: object`), 53 | params: definitionParams("file:///foo", "2:18"), 54 | want: locations("file:///foo", "4:2-4:5"), 55 | }, 56 | } 57 | 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | var h Handler 61 | 62 | tt.setup(t, &h) 63 | 64 | got, err := h.HandleDefinition(tt.params) 65 | 66 | if (err != nil) != tt.wantErr { 67 | t.Errorf("HandleDefinition() error = %v, wantErr %v", err, tt.wantErr) 68 | return 69 | } 70 | 71 | if tt.want == nil && got == nil { 72 | return 73 | } 74 | 75 | if !reflect.DeepEqual(got, tt.want) { 76 | t.Errorf("HandleDefinition() = %v, want %v", got, tt.want) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestHandler_HandleReferences(t *testing.T) { 83 | tests := []struct { 84 | name string 85 | setup HandlerSetupFunc 86 | params types.ReferenceParams 87 | want []types.Location 88 | wantErr bool 89 | }{ 90 | { 91 | name: "file not found", 92 | setup: loadFile("file:///foo", "foo"), 93 | params: referenceParams("file:///bar", "0:0"), 94 | }, 95 | { 96 | name: "no references", 97 | setup: loadFile("file:///foo", "foo"), 98 | params: referenceParams("file:///foo", "0:0"), 99 | }, 100 | { 101 | name: "simple", 102 | setup: loadFile("file:///foo", ` 103 | foo: 104 | $ref: "#/bar/baz" 105 | bar: 106 | baz: 107 | type: object`), 108 | params: referenceParams("file:///foo", "4:2"), 109 | want: locations("file:///foo", "2:9-2:18"), 110 | }, 111 | { 112 | name: "multiple references", 113 | setup: loadFile("file:///foo", ` 114 | foo: 115 | $ref: "#/bar/baz" 116 | foo2: 117 | $ref: "#/bar/baz" 118 | bar: 119 | baz: 120 | type: object`), 121 | params: referenceParams("file:///foo", "6:2"), 122 | want: locations("file:///foo", "2:9-2:18", "4:9-4:18"), 123 | }, 124 | } 125 | 126 | for _, tt := range tests { 127 | t.Run(tt.name, func(t *testing.T) { 128 | var h Handler 129 | 130 | tt.setup(t, &h) 131 | 132 | got, err := h.HandleReferences(tt.params) 133 | 134 | if (err != nil) != tt.wantErr { 135 | t.Errorf("HandleReferences() error = %v, wantErr %v", err, tt.wantErr) 136 | return 137 | } 138 | 139 | if len(tt.want) == 0 && len(got) == 0 { 140 | return 141 | } 142 | 143 | if !reflect.DeepEqual(got, tt.want) { 144 | t.Errorf("HandleReferences() = %v, want %v", got, tt.want) 145 | } 146 | }) 147 | } 148 | } 149 | 150 | func TestHandler_HandleChangeThenHandleDefinition(t *testing.T) { 151 | var h Handler 152 | 153 | // The file starts with only a definition. 154 | 155 | if err := h.HandleOpen(types.DidOpenTextDocumentParams{ 156 | TextDocument: types.TextDocumentItem{ 157 | URI: "file:///foo.yaml", 158 | Text: `bar: 159 | baz: 160 | type: object`, 161 | }, 162 | }); err != nil { 163 | t.Fatalf("HandleOpen: %v", err) 164 | } 165 | 166 | // Trigger a HandleDefinition call so that the yaml is parsed once. 167 | 168 | if _, err := h.HandleDefinition(types.DefinitionParams{ 169 | TextDocumentPositionParams: positionParams("file:///foo.yaml", "0:0"), 170 | }); err != nil { 171 | t.Fatalf("HandleDefinition: %v", err) 172 | } 173 | 174 | // Add the reference to the file. 175 | 176 | if err := h.HandleChange(types.DidChangeTextDocumentParams{ 177 | TextDocument: types.TextDocumentIdentifier{URI: "file:///foo.yaml"}, 178 | ContentChanges: []types.TextDocumentContentChangeEvent{{ 179 | Text: `foo: 180 | $ref: "#/bar/baz" 181 | `, 182 | Range: toPtr(newRange("0:0-0:0")), 183 | }}, 184 | }); err != nil { 185 | t.Fatalf("HandleChange: %v", err) 186 | } 187 | 188 | // Now that the reference has been added, we should be able to find the 189 | // definition. 190 | 191 | got, err := h.HandleDefinition(types.DefinitionParams{ 192 | TextDocumentPositionParams: positionParams("file:///foo.yaml", "1:8"), 193 | }) 194 | if err != nil { 195 | t.Fatalf("HandleDefinition: %v", err) 196 | } 197 | 198 | want := locations("file:///foo.yaml", "3:2-3:5") 199 | if !reflect.DeepEqual(got, want) { 200 | t.Errorf("HandleDefinition() = %v, want %v", got, want) 201 | } 202 | } 203 | 204 | func loadFile(uri, text string) HandlerSetupFunc { 205 | return func(t *testing.T, h *Handler) { 206 | if err := h.HandleOpen(types.DidOpenTextDocumentParams{ 207 | TextDocument: types.TextDocumentItem{ 208 | URI: uri, 209 | Text: text, 210 | }, 211 | }); err != nil { 212 | t.Fatal(err) 213 | } 214 | } 215 | } 216 | 217 | func referenceParams(uri, position string) types.ReferenceParams { 218 | return types.ReferenceParams{ 219 | TextDocumentPositionParams: positionParams(uri, position), 220 | } 221 | } 222 | 223 | func definitionParams(uri, position string) types.DefinitionParams { 224 | return types.DefinitionParams{ 225 | TextDocumentPositionParams: positionParams(uri, position), 226 | } 227 | } 228 | 229 | func positionParams(uri, position string) types.TextDocumentPositionParams { 230 | split := strings.Split(position, ":") 231 | 232 | line, err := strconv.Atoi(split[0]) 233 | if err != nil { 234 | panic(err) 235 | } 236 | 237 | character, err := strconv.Atoi(split[1]) 238 | if err != nil { 239 | panic(err) 240 | } 241 | 242 | return types.TextDocumentPositionParams{ 243 | TextDocument: types.TextDocumentIdentifier{ 244 | URI: uri, 245 | }, 246 | Position: types.Position{ 247 | Line: line, 248 | Character: character, 249 | }, 250 | } 251 | } 252 | 253 | func newRange(r string) types.Range { 254 | match := regexp.MustCompile(`^(\d+):(\d+)-(\d+):(\d+)$`).FindStringSubmatch(r) 255 | if match == nil { 256 | panic("invalid range") 257 | } 258 | 259 | return types.Range{ 260 | Start: types.Position{ 261 | Line: mustAtoi(match[1]), 262 | Character: mustAtoi(match[2]), 263 | }, 264 | End: types.Position{ 265 | Line: mustAtoi(match[3]), 266 | Character: mustAtoi(match[4]), 267 | }, 268 | } 269 | } 270 | 271 | func toPtr[T any](v T) *T { 272 | return &v 273 | } 274 | 275 | func locations(uri string, ranges ...string) []types.Location { 276 | locs := make([]types.Location, len(ranges)) 277 | 278 | for i, rng := range ranges { 279 | locs[i] = types.Location{ 280 | URI: uri, 281 | Range: newRange(rng), 282 | } 283 | } 284 | 285 | return locs 286 | } 287 | 288 | func mustAtoi(s string) int { 289 | i, err := strconv.Atoi(s) 290 | if err != nil { 291 | panic(err) 292 | } 293 | return i 294 | } 295 | -------------------------------------------------------------------------------- /internal/lsp/server_test.go: -------------------------------------------------------------------------------- 1 | package lsp_test 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "testing" 9 | 10 | "go.uber.org/mock/gomock" 11 | 12 | . "github.com/armsnyder/openapi-language-server/internal/lsp" 13 | "github.com/armsnyder/openapi-language-server/internal/lsp/jsonrpc" 14 | "github.com/armsnyder/openapi-language-server/internal/lsp/testutil" 15 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 16 | ) 17 | 18 | func TestServer_Basic(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | setup func(t *testing.T, s *Server, h *testutil.MockHandler) 22 | requests []string 23 | wantResponses []string 24 | }{ 25 | { 26 | name: "initialize with default capabilities", 27 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 28 | h.EXPECT().Capabilities().Return(types.ServerCapabilities{}) 29 | }, 30 | requests: []string{ 31 | `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`, 32 | }, 33 | wantResponses: []string{ 34 | `{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":{"change":0}},"serverInfo":{"name":"test-lsp","version":"0.1.0"}}}`, 35 | }, 36 | }, 37 | { 38 | name: "initialize with all capabilities", 39 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 40 | h.EXPECT().Capabilities().Return(types.ServerCapabilities{ 41 | TextDocumentSync: types.TextDocumentSyncOptions{ 42 | OpenClose: true, 43 | Change: types.SyncIncremental, 44 | }, 45 | DefinitionProvider: true, 46 | ReferencesProvider: true, 47 | }) 48 | }, 49 | requests: []string{ 50 | `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`, 51 | }, 52 | wantResponses: []string{ 53 | `{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"textDocumentSync":{"openClose":true,"change":2},"definitionProvider":true,"referencesProvider":true},"serverInfo":{"name":"test-lsp","version":"0.1.0"}}}`, 54 | }, 55 | }, 56 | { 57 | name: "initialized", 58 | requests: []string{ 59 | `{"jsonrpc":"2.0","id":1,"method":"initialized","params":{}}`, 60 | }, 61 | }, 62 | { 63 | name: "shutdown", 64 | requests: []string{ 65 | `{"jsonrpc":"2.0","id":1,"method":"shutdown","params":{}}`, 66 | }, 67 | wantResponses: []string{ 68 | `{"jsonrpc":"2.0","id":1,"result":null}`, 69 | }, 70 | }, 71 | { 72 | name: "textDocument/didOpen", 73 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 74 | h.EXPECT().HandleOpen(types.DidOpenTextDocumentParams{ 75 | TextDocument: types.TextDocumentItem{ 76 | URI: "file:///foo.txt", 77 | Text: "hello world", 78 | }, 79 | }).Return(nil) 80 | }, 81 | requests: []string{ 82 | `{"jsonrpc":"2.0","id":1,"method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///foo.txt","text":"hello world"}}}`, 83 | }, 84 | }, 85 | { 86 | name: "textDocument/didClose", 87 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 88 | h.EXPECT().HandleClose(types.DidCloseTextDocumentParams{ 89 | TextDocument: types.TextDocumentIdentifier{URI: "file:///foo.txt"}, 90 | }).Return(nil) 91 | }, 92 | requests: []string{ 93 | `{"jsonrpc":"2.0","id":1,"method":"textDocument/didClose","params":{"textDocument":{"uri":"file:///foo.txt"}}}`, 94 | }, 95 | }, 96 | { 97 | name: "textDocument/didChange full sync", 98 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 99 | h.EXPECT().HandleChange(types.DidChangeTextDocumentParams{ 100 | TextDocument: types.TextDocumentIdentifier{URI: "file:///foo.txt"}, 101 | ContentChanges: []types.TextDocumentContentChangeEvent{ 102 | {Text: "hello world"}, 103 | }, 104 | }).Return(nil) 105 | }, 106 | requests: []string{ 107 | `{"jsonrpc":"2.0","id":1,"method":"textDocument/didChange","params":{"textDocument":{"uri":"file:///foo.txt","version":42},"contentChanges":[{"text":"hello world"}]}}`, 108 | }, 109 | }, 110 | { 111 | name: "textDocument/didChange incremental sync", 112 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 113 | h.EXPECT().HandleChange(types.DidChangeTextDocumentParams{ 114 | TextDocument: types.TextDocumentIdentifier{URI: "file:///foo.txt"}, 115 | ContentChanges: []types.TextDocumentContentChangeEvent{{ 116 | Text: "carl", 117 | Range: &types.Range{Start: types.Position{Line: 0, Character: 6}, End: types.Position{Line: 0, Character: 10}}, 118 | }}, 119 | }).Return(nil) 120 | }, 121 | requests: []string{ 122 | `{"jsonrpc":"2.0","id":1,"method":"textDocument/didChange","params":{"textDocument":{"uri":"file:///foo.txt","version":42},"contentChanges":[{"text":"carl","range":{"start":{"line":0,"character":6},"end":{"line":0,"character":10}}}]}}`, 123 | }, 124 | }, 125 | { 126 | name: "textDocument/definition", 127 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 128 | h.EXPECT().HandleDefinition(types.DefinitionParams{ 129 | TextDocumentPositionParams: types.TextDocumentPositionParams{ 130 | TextDocument: types.TextDocumentIdentifier{URI: "file:///foo.txt"}, 131 | Position: types.Position{Line: 1, Character: 2}, 132 | }, 133 | }).Return([]types.Location{{ 134 | URI: "file:///bar.txt", 135 | Range: types.Range{Start: types.Position{Line: 3, Character: 4}, End: types.Position{Line: 5, Character: 6}}, 136 | }}, nil) 137 | }, 138 | requests: []string{ 139 | `{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{"textDocument":{"uri":"file:///foo.txt"},"position":{"line":1,"character":2}}}`, 140 | }, 141 | wantResponses: []string{ 142 | `{"jsonrpc":"2.0","id":1,"result":[{"uri":"file:///bar.txt","range":{"start":{"line":3,"character":4},"end":{"line":5,"character":6}}}]}`, 143 | }, 144 | }, 145 | { 146 | name: "textDocument/references", 147 | setup: func(t *testing.T, s *Server, h *testutil.MockHandler) { 148 | h.EXPECT().HandleReferences(types.ReferenceParams{ 149 | TextDocumentPositionParams: types.TextDocumentPositionParams{ 150 | TextDocument: types.TextDocumentIdentifier{URI: "file:///foo.txt"}, 151 | Position: types.Position{Line: 1, Character: 2}, 152 | }, 153 | }).Return([]types.Location{ 154 | { 155 | URI: "file:///bar.txt", 156 | Range: types.Range{Start: types.Position{Line: 3, Character: 4}, End: types.Position{Line: 5, Character: 6}}, 157 | }, 158 | }, nil) 159 | }, 160 | requests: []string{ 161 | `{"jsonrpc":"2.0","id":1,"method":"textDocument/references","params":{"textDocument":{"uri":"file:///foo.txt"},"position":{"line":1,"character":2}}}`, 162 | }, 163 | wantResponses: []string{ 164 | `{"jsonrpc":"2.0","id":1,"result":[{"uri":"file:///bar.txt","range":{"start":{"line":3,"character":4},"end":{"line":5,"character":6}}}]}`, 165 | }, 166 | }, 167 | { 168 | name: "unknown method", 169 | requests: []string{ 170 | `{"jsonrpc":"2.0","id":1,"method":"foo","params":{}}`, 171 | }, 172 | }, 173 | } 174 | 175 | for _, tt := range tests { 176 | t.Run(tt.name, func(t *testing.T) { 177 | ctrl := gomock.NewController(t) 178 | defer ctrl.Finish() 179 | 180 | handler := testutil.NewMockHandler(ctrl) 181 | reader := &bytes.Buffer{} 182 | writer := &bytes.Buffer{} 183 | server := Server{ 184 | ServerInfo: types.ServerInfo{ 185 | Name: "test-lsp", 186 | Version: "0.1.0", 187 | }, 188 | Handler: handler, 189 | Reader: reader, 190 | Writer: writer, 191 | } 192 | 193 | if tt.setup != nil { 194 | tt.setup(t, &server, handler) 195 | } 196 | 197 | send := RPCWriter{Writer: reader} 198 | for _, req := range tt.requests { 199 | fmt.Fprint(send, req) 200 | } 201 | 202 | if err := server.Run(); err != nil { 203 | t.Fatal("server.Run() error: ", err) 204 | } 205 | 206 | scanner := bufio.NewScanner(writer) 207 | scanner.Split(jsonrpc.Split) 208 | 209 | for _, want := range tt.wantResponses { 210 | if !scanner.Scan() { 211 | t.Fatal("missing response: ", want) 212 | } 213 | 214 | if got := scanner.Text(); got != want { 215 | t.Errorf("got response:\n%s\n\nexpected response:\n%s", got, want) 216 | } 217 | } 218 | 219 | if err := scanner.Err(); err != nil { 220 | t.Fatal("error while reading server responses: ", err) 221 | } 222 | }) 223 | } 224 | } 225 | 226 | type RPCWriter struct { 227 | Writer io.Writer 228 | } 229 | 230 | func (w RPCWriter) Write(p []byte) (n int, err error) { 231 | if err := jsonrpc.WritePayload(w.Writer, p); err != nil { 232 | return 0, err 233 | } 234 | 235 | return len(p), nil 236 | } 237 | 238 | var _ io.Writer = RPCWriter{} 239 | -------------------------------------------------------------------------------- /internal/analysis/yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml_test 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "strconv" 7 | "testing" 8 | 9 | . "github.com/armsnyder/openapi-language-server/internal/analysis/yaml" 10 | "github.com/armsnyder/openapi-language-server/internal/lsp/types" 11 | ) 12 | 13 | func TestParseLine(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | line string 17 | want Line 18 | }{ 19 | { 20 | name: "empty", 21 | line: "", 22 | want: Line{}, 23 | }, 24 | { 25 | name: "key only", 26 | line: "foo:", 27 | want: Line{ 28 | Key: "foo", 29 | KeyRange: types.Range{ 30 | Start: types.Position{Line: 0, Character: 0}, 31 | End: types.Position{Line: 0, Character: 3}, 32 | }, 33 | }, 34 | }, 35 | { 36 | name: "key and value", 37 | line: "foo: bar", 38 | want: Line{ 39 | Key: "foo", 40 | Value: "bar", 41 | KeyRange: types.Range{ 42 | Start: types.Position{Line: 0, Character: 0}, 43 | End: types.Position{Line: 0, Character: 3}, 44 | }, 45 | ValueRange: types.Range{ 46 | Start: types.Position{Line: 0, Character: 5}, 47 | End: types.Position{Line: 0, Character: 8}, 48 | }, 49 | }, 50 | }, 51 | { 52 | name: "key and value with leading whitespace", 53 | line: " foo: bar", 54 | want: Line{ 55 | Key: "foo", 56 | Value: "bar", 57 | KeyRange: types.Range{ 58 | Start: types.Position{Line: 0, Character: 2}, 59 | End: types.Position{Line: 0, Character: 5}, 60 | }, 61 | ValueRange: types.Range{ 62 | Start: types.Position{Line: 0, Character: 7}, 63 | End: types.Position{Line: 0, Character: 10}, 64 | }, 65 | }, 66 | }, 67 | { 68 | name: "double quoted value", 69 | line: `foo: "bar"`, 70 | want: Line{ 71 | Key: "foo", 72 | Value: "bar", 73 | KeyRange: types.Range{ 74 | Start: types.Position{Line: 0, Character: 0}, 75 | End: types.Position{Line: 0, Character: 3}, 76 | }, 77 | ValueRange: types.Range{ 78 | Start: types.Position{Line: 0, Character: 6}, 79 | End: types.Position{Line: 0, Character: 9}, 80 | }, 81 | }, 82 | }, 83 | { 84 | name: "single quoted value", 85 | line: `foo: 'bar'`, 86 | want: Line{ 87 | Key: "foo", 88 | Value: "bar", 89 | KeyRange: types.Range{ 90 | Start: types.Position{Line: 0, Character: 0}, 91 | End: types.Position{Line: 0, Character: 3}, 92 | }, 93 | ValueRange: types.Range{ 94 | Start: types.Position{Line: 0, Character: 6}, 95 | End: types.Position{Line: 0, Character: 9}, 96 | }, 97 | }, 98 | }, 99 | { 100 | name: "extra space before value", 101 | line: "foo: bar", 102 | want: Line{ 103 | Key: "foo", 104 | Value: "bar", 105 | KeyRange: types.Range{ 106 | Start: types.Position{Line: 0, Character: 0}, 107 | End: types.Position{Line: 0, Character: 3}, 108 | }, 109 | ValueRange: types.Range{ 110 | Start: types.Position{Line: 0, Character: 6}, 111 | End: types.Position{Line: 0, Character: 9}, 112 | }, 113 | }, 114 | }, 115 | } 116 | 117 | for _, tt := range tests { 118 | t.Run(tt.name, func(t *testing.T) { 119 | document, err := Parse(bytes.NewReader([]byte(tt.line + "\n"))) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | if len(document.Lines) != 1 { 124 | t.Fatalf("got %d lines, want 1", len(document.Lines)) 125 | } 126 | line := document.Lines[0] 127 | if line.Key != tt.want.Key { 128 | t.Errorf("got key %q, want %q", line.Key, tt.want.Key) 129 | } 130 | if line.Value != tt.want.Value { 131 | t.Errorf("got value %q, want %q", line.Value, tt.want.Value) 132 | } 133 | if len(line.Children) > 0 { 134 | t.Errorf("got %d children, want 0", len(line.Children)) 135 | } 136 | if line.Parent != nil { 137 | t.Errorf("got parent, want nil") 138 | } 139 | if line.KeyRange != tt.want.KeyRange { 140 | t.Errorf("got key range %v, want %v", line.KeyRange, tt.want.KeyRange) 141 | } 142 | if line.ValueRange != tt.want.ValueRange { 143 | t.Errorf("got value range %v, want %v", line.ValueRange, tt.want.ValueRange) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func TestParse(t *testing.T) { 150 | t.Run("empty", func(t *testing.T) { 151 | document, err := Parse(bytes.NewReader([]byte(""))) 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | if len(document.Lines) != 0 { 157 | t.Errorf("got %d lines, want 0", len(document.Lines)) 158 | } 159 | 160 | if len(document.Root) != 0 { 161 | t.Errorf("got %d root keys, want 0", len(document.Root)) 162 | } 163 | }) 164 | 165 | t.Run("one line", func(t *testing.T) { 166 | document, err := Parse(bytes.NewReader([]byte("foo: bar\n"))) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | if len(document.Lines) != 1 { 172 | t.Errorf("got %d lines, want 1", len(document.Lines)) 173 | } 174 | 175 | if len(document.Root) != 1 { 176 | t.Errorf("got %d root keys, want 1", len(document.Root)) 177 | } 178 | 179 | line := document.Lines[0] 180 | if line.Key != "foo" { 181 | t.Errorf("got key %q, want %q", line.Key, "foo") 182 | } 183 | 184 | if line.Value != "bar" { 185 | t.Errorf("got value %q, want %q", line.Value, "bar") 186 | } 187 | 188 | if len(line.Children) != 0 { 189 | t.Errorf("got %d children, want 0", len(line.Children)) 190 | } 191 | 192 | if line.Parent != nil { 193 | t.Errorf("got parent, want nil") 194 | } 195 | 196 | if document.Root["foo"] != line { 197 | t.Errorf("root key and line do not match") 198 | } 199 | }) 200 | 201 | t.Run("nested", func(t *testing.T) { 202 | document, err := Parse(bytes.NewReader([]byte("foo:\n bar: baz\n"))) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | 207 | if len(document.Lines) != 2 { 208 | t.Errorf("got %d lines, want 2", len(document.Lines)) 209 | } 210 | 211 | if len(document.Root) != 1 { 212 | t.Errorf("got %d root keys, want 1", len(document.Root)) 213 | } 214 | 215 | foo := document.Root["foo"] 216 | if foo == nil { 217 | t.Fatal("missing root key foo") 218 | } 219 | 220 | if foo.Key != "foo" { 221 | t.Errorf("foo: got key %q, want %q", foo.Key, "foo") 222 | } 223 | 224 | if foo.Value != "" { 225 | t.Errorf("foo: got value %q, want %q", foo.Value, "") 226 | } 227 | 228 | if len(foo.Children) != 1 { 229 | t.Errorf("foo: got %d children, want 1", len(foo.Children)) 230 | } 231 | 232 | if foo.Parent != nil { 233 | t.Errorf("foo: got parent, want nil") 234 | } 235 | 236 | bar := foo.Children["bar"] 237 | if bar == nil { 238 | t.Fatal("foo: missing child key bar") 239 | } 240 | 241 | if bar.Key != "bar" { 242 | t.Errorf("bar: got key %q, want %q", bar.Key, "bar") 243 | } 244 | 245 | if bar.Value != "baz" { 246 | t.Errorf("bar: got value %q, want %q", bar.Value, "baz") 247 | } 248 | 249 | if len(bar.Children) != 0 { 250 | t.Errorf("bar: got %d children, want 0", len(bar.Children)) 251 | } 252 | 253 | if bar.Parent != foo { 254 | t.Errorf("bar: parent mismatch") 255 | } 256 | 257 | if foo != document.Lines[0] { 258 | t.Errorf("foo line does not match") 259 | } 260 | 261 | if bar != document.Lines[1] { 262 | t.Errorf("bar line does not match") 263 | } 264 | }) 265 | } 266 | 267 | func TestRefs(t *testing.T) { 268 | document, err := Parse(bytes.NewReader([]byte(` 269 | # comment 270 | openapi: 3.0.0 271 | paths: 272 | /foo: 273 | get: 274 | $ref: "#/components/schemas/Foo" 275 | components: 276 | schemas: 277 | Foo: 278 | type: object 279 | Bar: 280 | type: object 281 | `))) 282 | if err != nil { 283 | t.Fatal(err) 284 | } 285 | 286 | const undefined = "undefined" 287 | 288 | expectedRefs := []string{ 289 | undefined, 290 | undefined, 291 | "#/openapi", 292 | "#/paths", 293 | undefined, 294 | undefined, 295 | undefined, 296 | "#/components", 297 | "#/components/schemas", 298 | "#/components/schemas/Foo", 299 | "#/components/schemas/Foo/type", 300 | "#/components/schemas/Bar", 301 | "#/components/schemas/Bar/type", 302 | } 303 | 304 | for i, expectedRef := range expectedRefs { 305 | if expectedRef == undefined { 306 | continue 307 | } 308 | 309 | t.Run(strconv.Itoa(i), func(t *testing.T) { 310 | line := document.Lines[i] 311 | gotRef := line.KeyRef() 312 | if gotRef != expectedRef { 313 | t.Errorf("KeyRef: got %q, want %q", gotRef, expectedRef) 314 | } 315 | 316 | if expectedRef != "" { 317 | loc := document.Locate(expectedRef) 318 | if loc != line { 319 | t.Errorf("Locate: got %v, want %v", loc, line) 320 | } 321 | } 322 | }) 323 | } 324 | } 325 | 326 | func TestParse_PetStore(t *testing.T) { 327 | f, err := os.Open("testdata/petstore.yaml") 328 | if err != nil { 329 | t.Fatal(err) 330 | } 331 | defer f.Close() 332 | 333 | document, err := Parse(f) 334 | if err != nil { 335 | t.Fatal(err) 336 | } 337 | 338 | line := document.Locate("#/components/schemas/Pet") 339 | if line == nil { 340 | t.Fatal("could not locate Pet schema") 341 | } 342 | 343 | wantRange := types.Range{ 344 | Start: types.Position{Line: 719, Character: 4}, 345 | End: types.Position{Line: 719, Character: 7}, 346 | } 347 | if line.KeyRange != wantRange { 348 | t.Errorf("got key range %v, want %v", line.KeyRange, wantRange) 349 | } 350 | 351 | wantKey := "Pet" 352 | if line.Key != wantKey { 353 | t.Errorf("got key %q, want %q", line.Key, wantKey) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /internal/analysis/yaml/testdata/petstore.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Swagger Petstore - OpenAPI 3.0 4 | description: |- 5 | This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about 6 | Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! 7 | You can now help us improve the API whether it's by making changes to the definition itself or to the code. 8 | That way, with time, we can improve the API in general, and expose some of the new features in OAS3. 9 | 10 | Some useful links: 11 | - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) 12 | - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) 13 | termsOfService: http://swagger.io/terms/ 14 | contact: 15 | email: apiteam@swagger.io 16 | license: 17 | name: Apache 2.0 18 | url: http://www.apache.org/licenses/LICENSE-2.0.html 19 | version: 1.0.19 20 | externalDocs: 21 | description: Find out more about Swagger 22 | url: http://swagger.io 23 | servers: 24 | - url: /api/v3 25 | tags: 26 | - name: pet 27 | description: Everything about your Pets 28 | externalDocs: 29 | description: Find out more 30 | url: http://swagger.io 31 | - name: store 32 | description: Access to Petstore orders 33 | externalDocs: 34 | description: Find out more about our store 35 | url: http://swagger.io 36 | - name: user 37 | description: Operations about user 38 | paths: 39 | /pet: 40 | put: 41 | tags: 42 | - pet 43 | summary: Update an existing pet 44 | description: Update an existing pet by Id 45 | operationId: updatePet 46 | requestBody: 47 | description: Update an existent pet in the store 48 | content: 49 | application/json: 50 | schema: 51 | $ref: "#/components/schemas/Pet" 52 | application/xml: 53 | schema: 54 | $ref: "#/components/schemas/Pet" 55 | application/x-www-form-urlencoded: 56 | schema: 57 | $ref: "#/components/schemas/Pet" 58 | required: true 59 | responses: 60 | "200": 61 | description: Successful operation 62 | content: 63 | application/xml: 64 | schema: 65 | $ref: "#/components/schemas/Pet" 66 | application/json: 67 | schema: 68 | $ref: "#/components/schemas/Pet" 69 | "400": 70 | description: Invalid ID supplied 71 | "404": 72 | description: Pet not found 73 | "405": 74 | description: Validation exception 75 | security: 76 | - petstore_auth: 77 | - write:pets 78 | - read:pets 79 | post: 80 | tags: 81 | - pet 82 | summary: Add a new pet to the store 83 | description: Add a new pet to the store 84 | operationId: addPet 85 | requestBody: 86 | description: Create a new pet in the store 87 | content: 88 | application/json: 89 | schema: 90 | $ref: "#/components/schemas/Pet" 91 | application/xml: 92 | schema: 93 | $ref: "#/components/schemas/Pet" 94 | application/x-www-form-urlencoded: 95 | schema: 96 | $ref: "#/components/schemas/Pet" 97 | required: true 98 | responses: 99 | "200": 100 | description: Successful operation 101 | content: 102 | application/xml: 103 | schema: 104 | $ref: "#/components/schemas/Pet" 105 | application/json: 106 | schema: 107 | $ref: "#/components/schemas/Pet" 108 | "405": 109 | description: Invalid input 110 | security: 111 | - petstore_auth: 112 | - write:pets 113 | - read:pets 114 | /pet/findByStatus: 115 | get: 116 | tags: 117 | - pet 118 | summary: Finds Pets by status 119 | description: Multiple status values can be provided with comma separated strings 120 | operationId: findPetsByStatus 121 | parameters: 122 | - name: status 123 | in: query 124 | description: Status values that need to be considered for filter 125 | required: false 126 | explode: true 127 | schema: 128 | type: string 129 | default: available 130 | enum: 131 | - available 132 | - pending 133 | - sold 134 | responses: 135 | "200": 136 | description: successful operation 137 | content: 138 | application/xml: 139 | schema: 140 | type: array 141 | items: 142 | $ref: "#/components/schemas/Pet" 143 | application/json: 144 | schema: 145 | type: array 146 | items: 147 | $ref: "#/components/schemas/Pet" 148 | "400": 149 | description: Invalid status value 150 | security: 151 | - petstore_auth: 152 | - write:pets 153 | - read:pets 154 | /pet/findByTags: 155 | get: 156 | tags: 157 | - pet 158 | summary: Finds Pets by tags 159 | description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 160 | operationId: findPetsByTags 161 | parameters: 162 | - name: tags 163 | in: query 164 | description: Tags to filter by 165 | required: false 166 | explode: true 167 | schema: 168 | type: array 169 | items: 170 | type: string 171 | responses: 172 | "200": 173 | description: successful operation 174 | content: 175 | application/xml: 176 | schema: 177 | type: array 178 | items: 179 | $ref: "#/components/schemas/Pet" 180 | application/json: 181 | schema: 182 | type: array 183 | items: 184 | $ref: "#/components/schemas/Pet" 185 | "400": 186 | description: Invalid tag value 187 | security: 188 | - petstore_auth: 189 | - write:pets 190 | - read:pets 191 | /pet/{petId}: 192 | get: 193 | tags: 194 | - pet 195 | summary: Find pet by ID 196 | description: Returns a single pet 197 | operationId: getPetById 198 | parameters: 199 | - name: petId 200 | in: path 201 | description: ID of pet to return 202 | required: true 203 | schema: 204 | type: integer 205 | format: int64 206 | responses: 207 | "200": 208 | description: successful operation 209 | content: 210 | application/xml: 211 | schema: 212 | $ref: "#/components/schemas/Pet" 213 | application/json: 214 | schema: 215 | $ref: "#/components/schemas/Pet" 216 | "400": 217 | description: Invalid ID supplied 218 | "404": 219 | description: Pet not found 220 | security: 221 | - api_key: [] 222 | - petstore_auth: 223 | - write:pets 224 | - read:pets 225 | post: 226 | tags: 227 | - pet 228 | summary: Updates a pet in the store with form data 229 | description: "" 230 | operationId: updatePetWithForm 231 | parameters: 232 | - name: petId 233 | in: path 234 | description: ID of pet that needs to be updated 235 | required: true 236 | schema: 237 | type: integer 238 | format: int64 239 | - name: name 240 | in: query 241 | description: Name of pet that needs to be updated 242 | schema: 243 | type: string 244 | - name: status 245 | in: query 246 | description: Status of pet that needs to be updated 247 | schema: 248 | type: string 249 | responses: 250 | "405": 251 | description: Invalid input 252 | security: 253 | - petstore_auth: 254 | - write:pets 255 | - read:pets 256 | delete: 257 | tags: 258 | - pet 259 | summary: Deletes a pet 260 | description: "" 261 | operationId: deletePet 262 | parameters: 263 | - name: api_key 264 | in: header 265 | description: "" 266 | required: false 267 | schema: 268 | type: string 269 | - name: petId 270 | in: path 271 | description: Pet id to delete 272 | required: true 273 | schema: 274 | type: integer 275 | format: int64 276 | responses: 277 | "400": 278 | description: Invalid pet value 279 | security: 280 | - petstore_auth: 281 | - write:pets 282 | - read:pets 283 | /pet/{petId}/uploadImage: 284 | post: 285 | tags: 286 | - pet 287 | summary: uploads an image 288 | description: "" 289 | operationId: uploadFile 290 | parameters: 291 | - name: petId 292 | in: path 293 | description: ID of pet to update 294 | required: true 295 | schema: 296 | type: integer 297 | format: int64 298 | - name: additionalMetadata 299 | in: query 300 | description: Additional Metadata 301 | required: false 302 | schema: 303 | type: string 304 | requestBody: 305 | content: 306 | application/octet-stream: 307 | schema: 308 | type: string 309 | format: binary 310 | responses: 311 | "200": 312 | description: successful operation 313 | content: 314 | application/json: 315 | schema: 316 | $ref: "#/components/schemas/ApiResponse" 317 | security: 318 | - petstore_auth: 319 | - write:pets 320 | - read:pets 321 | /store/inventory: 322 | get: 323 | tags: 324 | - store 325 | summary: Returns pet inventories by status 326 | description: Returns a map of status codes to quantities 327 | operationId: getInventory 328 | responses: 329 | "200": 330 | description: successful operation 331 | content: 332 | application/json: 333 | schema: 334 | type: object 335 | additionalProperties: 336 | type: integer 337 | format: int32 338 | security: 339 | - api_key: [] 340 | /store/order: 341 | post: 342 | tags: 343 | - store 344 | summary: Place an order for a pet 345 | description: Place a new order in the store 346 | operationId: placeOrder 347 | requestBody: 348 | content: 349 | application/json: 350 | schema: 351 | $ref: "#/components/schemas/Order" 352 | application/xml: 353 | schema: 354 | $ref: "#/components/schemas/Order" 355 | application/x-www-form-urlencoded: 356 | schema: 357 | $ref: "#/components/schemas/Order" 358 | responses: 359 | "200": 360 | description: successful operation 361 | content: 362 | application/json: 363 | schema: 364 | $ref: "#/components/schemas/Order" 365 | "405": 366 | description: Invalid input 367 | /store/order/{orderId}: 368 | get: 369 | tags: 370 | - store 371 | summary: Find purchase order by ID 372 | description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. 373 | operationId: getOrderById 374 | parameters: 375 | - name: orderId 376 | in: path 377 | description: ID of order that needs to be fetched 378 | required: true 379 | schema: 380 | type: integer 381 | format: int64 382 | responses: 383 | "200": 384 | description: successful operation 385 | content: 386 | application/xml: 387 | schema: 388 | $ref: "#/components/schemas/Order" 389 | application/json: 390 | schema: 391 | $ref: "#/components/schemas/Order" 392 | "400": 393 | description: Invalid ID supplied 394 | "404": 395 | description: Order not found 396 | delete: 397 | tags: 398 | - store 399 | summary: Delete purchase order by ID 400 | description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors 401 | operationId: deleteOrder 402 | parameters: 403 | - name: orderId 404 | in: path 405 | description: ID of the order that needs to be deleted 406 | required: true 407 | schema: 408 | type: integer 409 | format: int64 410 | responses: 411 | "400": 412 | description: Invalid ID supplied 413 | "404": 414 | description: Order not found 415 | /user: 416 | post: 417 | tags: 418 | - user 419 | summary: Create user 420 | description: This can only be done by the logged in user. 421 | operationId: createUser 422 | requestBody: 423 | description: Created user object 424 | content: 425 | application/json: 426 | schema: 427 | $ref: "#/components/schemas/User" 428 | application/xml: 429 | schema: 430 | $ref: "#/components/schemas/User" 431 | application/x-www-form-urlencoded: 432 | schema: 433 | $ref: "#/components/schemas/User" 434 | responses: 435 | default: 436 | description: successful operation 437 | content: 438 | application/json: 439 | schema: 440 | $ref: "#/components/schemas/User" 441 | application/xml: 442 | schema: 443 | $ref: "#/components/schemas/User" 444 | /user/createWithList: 445 | post: 446 | tags: 447 | - user 448 | summary: Creates list of users with given input array 449 | description: Creates list of users with given input array 450 | operationId: createUsersWithListInput 451 | requestBody: 452 | content: 453 | application/json: 454 | schema: 455 | type: array 456 | items: 457 | $ref: "#/components/schemas/User" 458 | responses: 459 | "200": 460 | description: Successful operation 461 | content: 462 | application/xml: 463 | schema: 464 | $ref: "#/components/schemas/User" 465 | application/json: 466 | schema: 467 | $ref: "#/components/schemas/User" 468 | default: 469 | description: successful operation 470 | /user/login: 471 | get: 472 | tags: 473 | - user 474 | summary: Logs user into the system 475 | description: "" 476 | operationId: loginUser 477 | parameters: 478 | - name: username 479 | in: query 480 | description: The user name for login 481 | required: false 482 | schema: 483 | type: string 484 | - name: password 485 | in: query 486 | description: The password for login in clear text 487 | required: false 488 | schema: 489 | type: string 490 | responses: 491 | "200": 492 | description: successful operation 493 | headers: 494 | X-Rate-Limit: 495 | description: calls per hour allowed by the user 496 | schema: 497 | type: integer 498 | format: int32 499 | X-Expires-After: 500 | description: date in UTC when token expires 501 | schema: 502 | type: string 503 | format: date-time 504 | content: 505 | application/xml: 506 | schema: 507 | type: string 508 | application/json: 509 | schema: 510 | type: string 511 | "400": 512 | description: Invalid username/password supplied 513 | /user/logout: 514 | get: 515 | tags: 516 | - user 517 | summary: Logs out current logged in user session 518 | description: "" 519 | operationId: logoutUser 520 | parameters: [] 521 | responses: 522 | default: 523 | description: successful operation 524 | /user/{username}: 525 | get: 526 | tags: 527 | - user 528 | summary: Get user by user name 529 | description: "" 530 | operationId: getUserByName 531 | parameters: 532 | - name: username 533 | in: path 534 | description: "The name that needs to be fetched. Use user1 for testing. " 535 | required: true 536 | schema: 537 | type: string 538 | responses: 539 | "200": 540 | description: successful operation 541 | content: 542 | application/xml: 543 | schema: 544 | $ref: "#/components/schemas/User" 545 | application/json: 546 | schema: 547 | $ref: "#/components/schemas/User" 548 | "400": 549 | description: Invalid username supplied 550 | "404": 551 | description: User not found 552 | put: 553 | tags: 554 | - user 555 | summary: Update user 556 | description: This can only be done by the logged in user. 557 | operationId: updateUser 558 | parameters: 559 | - name: username 560 | in: path 561 | description: name that needs to be updated 562 | required: true 563 | schema: 564 | type: string 565 | requestBody: 566 | description: Update an existent user in the store 567 | content: 568 | application/json: 569 | schema: 570 | $ref: "#/components/schemas/User" 571 | application/xml: 572 | schema: 573 | $ref: "#/components/schemas/User" 574 | application/x-www-form-urlencoded: 575 | schema: 576 | $ref: "#/components/schemas/User" 577 | responses: 578 | default: 579 | description: successful operation 580 | delete: 581 | tags: 582 | - user 583 | summary: Delete user 584 | description: This can only be done by the logged in user. 585 | operationId: deleteUser 586 | parameters: 587 | - name: username 588 | in: path 589 | description: The name that needs to be deleted 590 | required: true 591 | schema: 592 | type: string 593 | responses: 594 | "400": 595 | description: Invalid username supplied 596 | "404": 597 | description: User not found 598 | components: 599 | schemas: 600 | Order: 601 | type: object 602 | properties: 603 | id: 604 | type: integer 605 | format: int64 606 | example: 10 607 | petId: 608 | type: integer 609 | format: int64 610 | example: 198772 611 | quantity: 612 | type: integer 613 | format: int32 614 | example: 7 615 | shipDate: 616 | type: string 617 | format: date-time 618 | status: 619 | type: string 620 | description: Order Status 621 | example: approved 622 | enum: 623 | - placed 624 | - approved 625 | - delivered 626 | complete: 627 | type: boolean 628 | xml: 629 | name: order 630 | Customer: 631 | type: object 632 | properties: 633 | id: 634 | type: integer 635 | format: int64 636 | example: 100000 637 | username: 638 | type: string 639 | example: fehguy 640 | address: 641 | type: array 642 | xml: 643 | name: addresses 644 | wrapped: true 645 | items: 646 | $ref: "#/components/schemas/Address" 647 | xml: 648 | name: customer 649 | Address: 650 | type: object 651 | properties: 652 | street: 653 | type: string 654 | example: 437 Lytton 655 | city: 656 | type: string 657 | example: Palo Alto 658 | state: 659 | type: string 660 | example: CA 661 | zip: 662 | type: string 663 | example: "94301" 664 | xml: 665 | name: address 666 | Category: 667 | type: object 668 | properties: 669 | id: 670 | type: integer 671 | format: int64 672 | example: 1 673 | name: 674 | type: string 675 | example: Dogs 676 | xml: 677 | name: category 678 | User: 679 | type: object 680 | properties: 681 | id: 682 | type: integer 683 | format: int64 684 | example: 10 685 | username: 686 | type: string 687 | example: theUser 688 | firstName: 689 | type: string 690 | example: John 691 | lastName: 692 | type: string 693 | example: James 694 | email: 695 | type: string 696 | example: john@email.com 697 | password: 698 | type: string 699 | example: "12345" 700 | phone: 701 | type: string 702 | example: "12345" 703 | userStatus: 704 | type: integer 705 | description: User Status 706 | format: int32 707 | example: 1 708 | xml: 709 | name: user 710 | Tag: 711 | type: object 712 | properties: 713 | id: 714 | type: integer 715 | format: int64 716 | name: 717 | type: string 718 | xml: 719 | name: tag 720 | Pet: 721 | required: 722 | - name 723 | - photoUrls 724 | type: object 725 | properties: 726 | id: 727 | type: integer 728 | format: int64 729 | example: 10 730 | name: 731 | type: string 732 | example: doggie 733 | category: 734 | $ref: "#/components/schemas/Category" 735 | photoUrls: 736 | type: array 737 | xml: 738 | wrapped: true 739 | items: 740 | type: string 741 | xml: 742 | name: photoUrl 743 | tags: 744 | type: array 745 | xml: 746 | wrapped: true 747 | items: 748 | $ref: "#/components/schemas/Tag" 749 | status: 750 | type: string 751 | description: pet status in the store 752 | enum: 753 | - available 754 | - pending 755 | - sold 756 | xml: 757 | name: pet 758 | ApiResponse: 759 | type: object 760 | properties: 761 | code: 762 | type: integer 763 | format: int32 764 | type: 765 | type: string 766 | message: 767 | type: string 768 | xml: 769 | name: "##default" 770 | requestBodies: 771 | Pet: 772 | description: Pet object that needs to be added to the store 773 | content: 774 | application/json: 775 | schema: 776 | $ref: "#/components/schemas/Pet" 777 | application/xml: 778 | schema: 779 | $ref: "#/components/schemas/Pet" 780 | UserArray: 781 | description: List of user object 782 | content: 783 | application/json: 784 | schema: 785 | type: array 786 | items: 787 | $ref: "#/components/schemas/User" 788 | securitySchemes: 789 | petstore_auth: 790 | type: oauth2 791 | flows: 792 | implicit: 793 | authorizationUrl: https://petstore3.swagger.io/oauth/authorize 794 | scopes: 795 | write:pets: modify pets in your account 796 | read:pets: read your pets 797 | api_key: 798 | type: apiKey 799 | name: api_key 800 | in: header 801 | --------------------------------------------------------------------------------