├── .editorconfig
├── .github
└── workflows
│ ├── codeql-analysis.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── apidoc.go
├── apidoc_test.go
├── build
├── build.go
├── build_test.go
├── config.go
├── config_test.go
├── detect.go
├── detect_test.go
├── input.go
├── input_test.go
├── output.go
├── output_test.go
├── path.go
├── path_test.go
├── path_windows_test.go
└── testdata
│ ├── failed.yaml
│ ├── gbk.php
│ ├── no-extension
│ ├── testdir1
│ ├── testfile.1
│ └── testfile.2
│ ├── testdir2
│ └── testfile.1
│ ├── testfile.1
│ ├── testfile.c
│ └── testfile.h
├── cmd
└── apidoc
│ ├── .gitignore
│ └── main.go
├── core
├── core.go
├── core_test.go
├── errors.go
├── errors_test.go
├── gbk.php
├── message.go
├── message_test.go
├── messagetest
│ ├── messagetest.go
│ └── messagetest_test.go
├── uri.go
├── uri_test.go
├── version.go
└── version_test.go
├── docs
├── CNAME
├── README.md
├── docs.go
├── example
│ ├── .apidoc.yaml
│ ├── .gitignore
│ ├── apis.cpp
│ ├── apis.rs
│ ├── doc.cpp
│ └── index.xml
├── icon.svg
├── index.cmn-Hant.xml
├── index.css
├── index.js
├── index.xml
├── index.xsl
├── locale.cmn-Hans.xml
├── locale.cmn-Hant.xml
├── site.xml
├── v5
│ ├── apidoc.css
│ ├── apidoc.js
│ ├── apidoc.xsl
│ ├── locales.xsl
│ └── view.html
└── v6
│ ├── apidoc.css
│ ├── apidoc.js
│ ├── apidoc.xsl
│ ├── locales.xsl
│ └── view.html
├── go.mod
├── go.sum
├── internal
├── ast
│ ├── ast.go
│ ├── ast_test.go
│ ├── asttest
│ │ ├── asttest.go
│ │ ├── asttest_test.go
│ │ ├── gen.go
│ │ ├── index.xml
│ │ └── make.go
│ ├── attributes.go
│ ├── attributes_test.go
│ ├── elements.go
│ ├── elements_test.go
│ ├── parse.go
│ ├── parse_test.go
│ ├── sanitize.go
│ ├── sanitize_test.go
│ ├── search.go
│ ├── search_test.go
│ └── testdata
│ │ ├── all.xml
│ │ ├── api.xml
│ │ └── doc.xml
├── cmd
│ ├── build.go
│ ├── cmd.go
│ ├── cmd_test.go
│ ├── detect.go
│ ├── detect_test.go
│ ├── lang.go
│ ├── lang_test.go
│ ├── locale.go
│ ├── locale_test.go
│ ├── lsp.go
│ ├── mock.go
│ ├── mock_test.go
│ ├── static.go
│ ├── syntax.go
│ ├── syntax_test.go
│ ├── version.go
│ └── version_test.go
├── docs
│ ├── docs.go
│ ├── docs_test.go
│ ├── gen.go
│ ├── make_site.go
│ └── site
│ │ ├── command.go
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── site.go
│ │ ├── site_test.go
│ │ ├── spec.go
│ │ └── spec_test.go
├── lang
│ ├── block.go
│ ├── block_test.go
│ ├── lang.go
│ ├── lang_test.go
│ ├── nim.go
│ ├── nim_test.go
│ ├── parse.go
│ ├── parse_test.go
│ ├── pascal.go
│ ├── pascal_test.go
│ ├── php.go
│ ├── php_test.go
│ ├── ruby.go
│ ├── ruby_test.go
│ ├── swift.go
│ ├── swift_test.go
│ ├── testdata
│ │ ├── c#
│ │ │ └── test.cs
│ │ ├── c++
│ │ │ └── test.cpp
│ │ ├── d
│ │ │ └── test.d
│ │ ├── dart
│ │ │ └── test.dart
│ │ ├── erlang
│ │ │ └── test.erl
│ │ ├── go
│ │ │ └── test.go
│ │ ├── groovy
│ │ │ └── test.groovy
│ │ ├── java
│ │ │ └── test.java
│ │ ├── javascript
│ │ │ └── test.js
│ │ ├── julia
│ │ │ └── test.jl
│ │ ├── kotlin
│ │ │ └── test.kt
│ │ ├── lisp
│ │ │ └── test.clj
│ │ ├── lua
│ │ │ └── test.lua
│ │ ├── nim
│ │ │ └── test.nim
│ │ ├── pascal
│ │ │ └── test.pas
│ │ ├── perl
│ │ │ └── test.perl
│ │ ├── php
│ │ │ └── test.php
│ │ ├── python
│ │ │ └── test.py
│ │ ├── ruby
│ │ │ └── test.rb
│ │ ├── rust
│ │ │ └── test.rs
│ │ ├── scala
│ │ │ └── test.scala
│ │ ├── swift
│ │ │ └── test.swift
│ │ ├── typescript
│ │ │ └── test.ts
│ │ └── zig
│ │ │ └── test.zig
│ └── testdata_test.go
├── lexer
│ ├── lexer.go
│ ├── lexer_test.go
│ ├── position.go
│ └── position_test.go
├── locale
│ ├── locale.go
│ ├── locale_test.go
│ ├── message.go
│ ├── message_cmn-Hans.go
│ └── message_cmn-Hant.go
├── lsp
│ ├── apidoc.go
│ ├── errors.go
│ ├── folder.go
│ ├── folder_test.go
│ ├── hover.go
│ ├── hover_test.go
│ ├── initialize.go
│ ├── initialize_test.go
│ ├── lsp.go
│ ├── lsp_test.go
│ ├── protocol
│ │ ├── apidoc.go
│ │ ├── apidoc_test.go
│ │ ├── completion.go
│ │ ├── completion_test.go
│ │ ├── diagnostic.go
│ │ ├── diagnostic_test.go
│ │ ├── folding.go
│ │ ├── folding_test.go
│ │ ├── hover.go
│ │ ├── hover_test.go
│ │ ├── initialize.go
│ │ ├── initialize_test.go
│ │ ├── message.go
│ │ ├── message_test.go
│ │ ├── protocol.go
│ │ ├── reference.go
│ │ ├── semantic.go
│ │ ├── server.go
│ │ ├── textdocument.go
│ │ ├── textdocument_test.go
│ │ ├── workspace.go
│ │ └── workspace_test.go
│ ├── reference.go
│ ├── reference_test.go
│ ├── semantic.go
│ ├── semantic_test.go
│ ├── server.go
│ ├── server_test.go
│ ├── textdocument.go
│ ├── textdocument_test.go
│ ├── workspace.go
│ └── workspace_test.go
├── mock
│ ├── api.go
│ ├── api_test.go
│ ├── json.go
│ ├── json_test.go
│ ├── mock.go
│ ├── mock_test.go
│ ├── options.go
│ ├── options_test.go
│ ├── xml.go
│ └── xml_test.go
├── node
│ ├── node.go
│ ├── node_test.go
│ ├── value.go
│ └── value_test.go
├── openapi
│ ├── errors.go
│ ├── info.go
│ ├── info_test.go
│ ├── openapi.go
│ ├── openapi_test.go
│ ├── parameter.go
│ ├── parameter_test.go
│ ├── parse.go
│ ├── parse_test.go
│ ├── path.go
│ ├── schema.go
│ ├── schema_test.go
│ ├── security.go
│ ├── server.go
│ ├── server_test.go
│ ├── style.go
│ └── style_test.go
└── xmlenc
│ ├── decode.go
│ ├── decode_test.go
│ ├── encode.go
│ ├── encode_test.go
│ ├── parser.go
│ ├── parser_test.go
│ ├── token.go
│ ├── token_test.go
│ ├── xmlenc.go
│ └── xmlenc_test.go
├── mock.go
├── mock_test.go
└── scripts
├── build-env.sh
├── build.cmd
├── build.sh
└── install.sh
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | charset = utf-8
11 |
12 | # html
13 | [*.{htm,html,js,css}]
14 | indent_style = space
15 | indent_size = 4
16 |
17 | # yaml
18 | [*.{yaml,yml}]
19 | indent_style = space
20 | indent_size = 4
21 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | - cron: '0 5 * * 6'
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ['go']
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 | with:
35 | # We must fetch at least the immediate parents so that if this is
36 | # a pull request then we can checkout the head.
37 | fetch-depth: 2
38 |
39 | # Initializes the CodeQL tools for scanning.
40 | - name: Initialize CodeQL
41 | uses: github/codeql-action/init@v2
42 | with:
43 | languages: ${{ matrix.language }}
44 | # If you wish to specify custom queries, you can do so here or in a config file.
45 | # By default, queries listed here will override any specified in a config file.
46 | # Prefix the list here with "+" to use these queries and those in the config file.
47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
48 |
49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
50 | # If this step fails, then you should remove it and run the build manually (see below)
51 | - name: Autobuild
52 | uses: github/codeql-action/autobuild@v2
53 |
54 | # ℹ️ Command-line programs to run using the OS shell.
55 | # 📚 https://git.io/JvXDl
56 |
57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
58 | # and modify them (or add more) to build your code if your project
59 | # uses a compiled language
60 |
61 | #- run: |
62 | # make bootstrap
63 | # make release
64 |
65 | - name: Perform CodeQL Analysis
66 | uses: github/codeql-action/analyze@v2
67 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | create:
4 | tags:
5 | - 'v*'
6 | - '!v*-alpha'
7 | - '!v*-beta'
8 |
9 | jobs:
10 |
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 |
17 | - name: setup Go
18 | uses: actions/setup-go@v2
19 | with:
20 | go-version: 1.19.x
21 | id: go
22 |
23 | - name: Check out code into the Go module directory
24 | uses: actions/checkout@v2
25 |
26 | - name: Login to Docker Hub
27 | uses: docker/login-action@v1
28 | with:
29 | username: ${{ secrets.DOCKER_HUB_USERNAME }}
30 | password: ${{ secrets.DOCKER_HUB_TOKEN }}
31 |
32 | - name: Login to Github Container Registry
33 | uses: docker/login-action@v1
34 | with:
35 | registry: ghcr.io
36 | username: ${{ github.repository_owner }}
37 | password: ${{ secrets.GHCR_TOKEN }}
38 |
39 | - name: release
40 | uses: goreleaser/goreleaser-action@v2
41 | with:
42 | version: latest
43 | args: release
44 | env:
45 | # 如果需要操作其它仓库,比如将 brew 写入其它仓库中,
46 | # 则不能使用默认的 GITHUB_TOKEN,需要自行创建。
47 | GITHUB_TOKEN: ${{ secrets.HOMEBREW }}
48 |
49 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push, pull_request]
3 |
4 | jobs:
5 |
6 | test:
7 | name: Test
8 | runs-on: ${{ matrix.os }}
9 |
10 | strategy:
11 | matrix:
12 | os: [ubuntu-latest, macOS-latest, windows-latest]
13 | go: ['1.18.x', '1.19.x']
14 |
15 | steps:
16 |
17 | - name: Set git to use LF
18 | run: |
19 | git config --global core.autocrlf false
20 | git config --global core.eol lf
21 |
22 | - name: Check out code into the Go module directory
23 | uses: actions/checkout@v2
24 |
25 | - name: Set up Go ${{ matrix.go }}
26 | uses: actions/setup-go@v2
27 | with:
28 | stable: '!contains(${{ matrix.go }}, "beta") && !contains(${{ matrix.go }}, "rc")'
29 | go-version: ${{ matrix.go }}
30 | id: go
31 |
32 | - name: generate
33 | run: go generate -v ./...
34 |
35 | - name: Vet
36 | run: go vet -v ./...
37 |
38 | - name: Test
39 | env:
40 | LANG: en # 必须指定语言,否则 internal/locale 测试不通过
41 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
42 |
43 | - name: Upload Coverage report
44 | uses: codecov/codecov-action@v1
45 | with:
46 | token: ${{secrets.CODECOV_TOKEN}}
47 | file: ./coverage.txt
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | *.exe
7 | *.test
8 | *.prof
9 |
10 | # vim
11 | *.swp
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | # goreleaser
17 | dist
18 |
19 | # project
20 | .apidoc.yaml
21 | .apidoc.yml
22 | apidoc.json
23 | apidoc.yaml
24 | apidoc.xml
25 | openapi.json
26 | openapi.yaml
27 |
28 | .vscode
29 | .idea
30 |
31 | .testdata
32 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | project_name: apidoc
2 |
3 | release:
4 | github:
5 | owner: caixw
6 | name: apidoc
7 | name_template: '{{.Tag}}'
8 |
9 | builds:
10 | - id: apidoc
11 |
12 | goos:
13 | - darwin
14 | - linux
15 | - windows
16 |
17 | goarch:
18 | - amd64
19 | - arm64
20 |
21 | ignore:
22 | - goos: windows
23 | goarch: arm64
24 |
25 | main: ./cmd/apidoc
26 |
27 | binary: apidoc
28 |
29 | flags:
30 | - -trimpath
31 |
32 | ldflags:
33 | - -s -w -X github.com/caixw/apidoc/v7/core.metadata={{time "20060102"}}.{{.Commit}} -X github.com/caixw/apidoc/v7/core.mainVersion={{.Tag}}
34 |
35 | env:
36 | - CGO_ENABLED=0
37 |
38 | brews:
39 | - tap:
40 | owner: caixw
41 | name: homebrew-brew
42 | url_template: "https://github.com/caixw/apidoc/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
43 |
44 | commit_author:
45 | name: goreleaserbot
46 | email: goreleaser@carlosbecker.com
47 | folder: Formula
48 | homepage: "https://apidoc.tools"
49 | description: RESTful API 文档生成工具
50 | license: MIT
51 |
52 |
53 | dockers:
54 | -
55 | ids: ['apidoc']
56 | goos: linux
57 | goarch: amd64
58 | dockerfile: Dockerfile
59 |
60 | image_templates:
61 | - "docker.io/caixw/apidoc:{{ .Tag }}"
62 | - "docker.io/caixw/apidoc:v{{ .Major }}"
63 | - "docker.io/caixw/apidoc:v{{ .Major }}.{{ .Minor }}"
64 | - "docker.io/caixw/apidoc:latest"
65 |
66 | - "ghcr.io/caixw/apidoc:{{ .Tag }}"
67 | - "ghcr.io/caixw/apidoc:v{{ .Major }}"
68 | - "ghcr.io/caixw/apidoc:v{{ .Major }}.{{ .Minor }}"
69 | - "ghcr.io/caixw/apidoc:latest"
70 |
71 |
72 | archives:
73 | - builds:
74 | - apidoc
75 | replacements:
76 | darwin: macOS
77 | format_overrides:
78 | - goos: windows
79 | format: zip
80 | files:
81 | - licence*
82 | - LICENCE*
83 | - license*
84 | - LICENSE*
85 | - readme*
86 | - README*
87 | - changelog*
88 | - CHANGELOG*
89 |
90 | checksum:
91 | name_template: checksums.txt
92 | algorithm: sha256
93 |
94 | changelog:
95 | skip: true
96 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 如何为 apidoc 贡献代码
2 |
3 | apidoc 是一个基于 [MIT](https://opensource.org/licenses/MIT) 的开源软件。
4 | 欢迎大家共同参与开发。**若需要新功能,请先开 issue 讨论。**
5 |
6 | ## 本地化
7 |
8 | 本地化包含以下几个部分:
9 |
10 | - `internal/locale` 主要包含了程序内各种语法错误以及命令行的提示信息;
11 | - `docs/vx/locales.xsl` 包含展示界面中的本化元素;`vx` 表示版本信息,比如 `v5`、`v6` 等;
12 | - `docs/index.*.xml` 网站的内容,* 表示语言 ID;
13 |
14 | ## 文档
15 |
16 | 可以通过 `apidoc static -docs=xxx` 将 docs 作为一个本地的 web 服务,方便 XSL 相关功能的调试;
17 |
18 | 文档应该尽可能的保证在非 Javascript 环境下,也有基本的功能。
19 |
20 | ## 添加新编程语言支持
21 |
22 | `internal/lang/lang.go` 文件中有所有语言模型的定义,在该文件中有详细的文档说明如何定义语言模型。若需要添加对新语言的支持,提交并更新 [#11](https://github.com/caixw/apidoc/issues/11)
23 |
24 | ## Git
25 |
26 | Git 的编写规则参照 ,
27 | scope 值可直接写包名,或是文件名,比如 `internal/mock`、`README.md`。
28 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # apidoc dockerfile
2 |
3 | FROM scratch
4 |
5 | MAINTAINER caixw
6 |
7 | COPY ./apidoc /
8 |
9 | ENTRYPOINT ["/apidoc"]
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 caixw
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 |
23 |
--------------------------------------------------------------------------------
/build/build.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // Package build 提供构建文档的相关功能
4 | package build
5 |
6 | import (
7 | "bytes"
8 |
9 | "github.com/caixw/apidoc/v7/core"
10 | "github.com/caixw/apidoc/v7/internal/ast"
11 | )
12 |
13 | // Build 解析文档并输出文档内容
14 | //
15 | // 如果是配置文件有问题,则直接返回错误信息,文档错误则输出至 h 对象。
16 | func Build(h *core.MessageHandler, o *Output, i ...*Input) error {
17 | d, err := parse(h, i...)
18 | if err != nil {
19 | return err
20 | }
21 | if err = o.sanitize(); err != nil {
22 | return err
23 | }
24 |
25 | buf, err := o.buffer(d)
26 | if err != nil {
27 | return err
28 | }
29 |
30 | return o.Path.WriteAll(buf.Bytes())
31 | }
32 |
33 | // Buffer 生成文档内容并返回
34 | //
35 | // 如果是配置文件有问题,则直接返回错误信息,文档错误则输出至 h 对象。
36 | func Buffer(h *core.MessageHandler, o *Output, i ...*Input) (*bytes.Buffer, error) {
37 | d, err := parse(h, i...)
38 | if err != nil {
39 | return nil, err
40 | }
41 | if err = o.sanitize(); err != nil {
42 | return nil, err
43 | }
44 |
45 | return o.buffer(d)
46 | }
47 |
48 | // CheckSyntax 测试文档语法
49 | //
50 | // 如果是配置文件有问题,则直接返回错误信息,文档错误则输出至 h 对象。
51 | func CheckSyntax(h *core.MessageHandler, i ...*Input) error {
52 | _, err := parse(h, i...)
53 | return err
54 | }
55 |
56 | func parse(h *core.MessageHandler, i ...*Input) (*ast.APIDoc, error) {
57 | for _, item := range i {
58 | if err := item.sanitize(); err != nil {
59 | return nil, err
60 | }
61 | }
62 |
63 | d := &ast.APIDoc{}
64 | d.ParseBlocks(h, func(blocks chan core.Block) {
65 | ParseInputs(blocks, h, i...)
66 | })
67 |
68 | return d, nil
69 | }
70 |
--------------------------------------------------------------------------------
/build/build_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package build
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core/messagetest"
11 | )
12 |
13 | func TestParse(t *testing.T) {
14 | a := assert.New(t, false)
15 |
16 | php := &Input{
17 | Lang: "php",
18 | Dir: "./testdata",
19 | Recursive: true,
20 | Encoding: "gbk",
21 | }
22 |
23 | c := &Input{
24 | Lang: "c++",
25 | Dir: "./testdata",
26 | Recursive: true,
27 | }
28 |
29 | rslt := messagetest.NewMessageHandler()
30 | doc, err := parse(rslt.Handler, php, c)
31 | a.NotError(err).NotNil(doc)
32 | rslt.Handler.Stop()
33 | a.Empty(rslt.Errors)
34 |
35 | a.Equal(2, len(doc.APIs)).
36 | Equal(doc.Version.V(), "1.1.1")
37 | api := doc.APIs[0]
38 | a.Equal(api.Method.V(), "GET")
39 | }
40 |
--------------------------------------------------------------------------------
/build/detect.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package build
4 |
5 | import (
6 | "os"
7 | "path/filepath"
8 | "sort"
9 | "strings"
10 |
11 | "github.com/caixw/apidoc/v7/core"
12 | "github.com/caixw/apidoc/v7/internal/ast"
13 | "github.com/caixw/apidoc/v7/internal/lang"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | )
16 |
17 | // DetectConfig 检测 wd 内容并生成 Config 实例
18 | //
19 | // wd 只能为本地文件系统;
20 | // recursive 是否检测子目录;
21 | func DetectConfig(wd core.URI, recursive bool) (*Config, error) {
22 | scheme, path := wd.Parse()
23 | if scheme != "" && scheme != core.SchemeFile {
24 | panic("参数 wd 只能为本地文件")
25 | }
26 |
27 | inputs, err := detectInput(path, recursive)
28 | if err != nil {
29 | return nil, err
30 | }
31 | if len(inputs) == 0 {
32 | return nil, core.NewError(locale.ErrNotFoundSupportedLang)
33 | }
34 |
35 | cfg := &Config{
36 | Version: ast.Version,
37 | Inputs: inputs,
38 | Output: &Output{
39 | Path: "./apidoc.xml",
40 | },
41 | }
42 |
43 | if err = cfg.sanitize(wd); err != nil {
44 | return nil, err
45 | }
46 | return cfg, nil
47 | }
48 |
49 | // 检测指定目录下的内容,并为其生成一个合适的 Input 实例。
50 | //
51 | // 检测依据为根据扩展名来做统计,数量最大且被支持的获胜。
52 | func detectInput(dir string, recursive bool) ([]*Input, error) {
53 | exts, err := detectExts(dir, recursive)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | langs := detectLanguage(exts)
59 |
60 | opts := make([]*Input, 0, len(langs))
61 | for _, l := range langs {
62 | opts = append(opts, &Input{
63 | Lang: l.ID,
64 | Dir: "./",
65 | Exts: l.Exts,
66 | Recursive: recursive,
67 | })
68 | }
69 |
70 | return opts, nil
71 | }
72 |
73 | type language struct {
74 | lang.Language
75 | count int
76 | }
77 |
78 | // 根据 exts 计算每个语言对应的文件数量,并按倒序返回
79 | //
80 | // exts 参数为从 detectExts 中获取的返回值
81 | func detectLanguage(exts map[string]int) []*language {
82 | langs := make([]*language, 0, len(exts))
83 |
84 | for ext, count := range exts {
85 | l := lang.GetByExt(ext)
86 | if l == nil {
87 | continue
88 | }
89 |
90 | found := false
91 | for _, item := range langs {
92 | if item.ID == l.ID {
93 | item.count += count
94 | found = true
95 | break
96 | }
97 | }
98 | if !found {
99 | langs = append(langs, &language{
100 | count: count,
101 | Language: *l,
102 | })
103 | }
104 | } // end for
105 |
106 | sort.SliceStable(langs, func(i, j int) bool {
107 | return langs[i].count > langs[j].count
108 | })
109 |
110 | return langs
111 | }
112 |
113 | // 返回 dir 目录下文件类型及对应的文件数量的一个集合。
114 | // recursive 表示是否查找子目录。
115 | func detectExts(dir string, recursive bool) (map[string]int, error) {
116 | exts := map[string]int{}
117 |
118 | walk := func(path string, fi os.FileInfo, err error) error {
119 | if err != nil {
120 | return err
121 | }
122 |
123 | if fi.IsDir() {
124 | if !recursive && dir != path {
125 | return filepath.SkipDir
126 | }
127 | } else {
128 | ext := strings.ToLower(filepath.Ext(path))
129 | if len(ext) > 0 {
130 | exts[ext]++
131 | }
132 | }
133 |
134 | return nil
135 | }
136 |
137 | if err := filepath.Walk(dir, walk); err != nil {
138 | return nil, err
139 | }
140 |
141 | return exts, nil
142 | }
143 |
--------------------------------------------------------------------------------
/build/detect_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package build
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | )
10 |
11 | func TestDetectInput(t *testing.T) {
12 | a := assert.New(t, false)
13 |
14 | o, err := detectInput("./testdata", true)
15 | a.NotError(err).NotEmpty(o)
16 | a.Equal(len(o), 2). // c and php
17 | Equal(o[0].Lang, "c++").
18 | Equal(o[1].Lang, "php")
19 | }
20 |
21 | func TestDetectLanguage(t *testing.T) {
22 | a := assert.New(t, false)
23 | exts := map[string]int{
24 | ".h": 2,
25 | ".c": 3,
26 | ".swift": 1,
27 | ".php": 2,
28 | }
29 |
30 | langs := detectLanguage(exts)
31 | a.Equal(len(langs), 3) // c++,php,swift
32 | a.Equal(langs[0].ID, "c++").
33 | Equal(langs[0].count, 5)
34 | a.Equal(langs[1].ID, "php").
35 | Equal(langs[1].count, 2)
36 | a.Equal(langs[2].ID, "swift").
37 | Equal(langs[2].count, 1)
38 | }
39 |
40 | func TestDetectExts(t *testing.T) {
41 | a := assert.New(t, false)
42 |
43 | files, err := detectExts("./testdata", false)
44 | a.NotError(err)
45 | a.Equal(len(files), 5)
46 | a.Equal(files[".php"], 1).Equal(files[".c"], 1)
47 |
48 | files, err = detectExts("./testdata", true)
49 | a.NotError(err)
50 | a.Equal(len(files), 6)
51 | a.Equal(files[".php"], 1).Equal(files[".1"], 3)
52 | }
53 |
--------------------------------------------------------------------------------
/build/output_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package build
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | "github.com/caixw/apidoc/v7/internal/ast/asttest"
12 | "github.com/caixw/apidoc/v7/internal/docs"
13 | )
14 |
15 | func TestOptions_contains(t *testing.T) {
16 | a := assert.New(t, false)
17 |
18 | o := &Output{}
19 | a.True(o.contains("tag"))
20 | a.True(o.contains(""))
21 |
22 | o.Tags = []string{"t1", "t2"}
23 | a.True(o.contains("t1"))
24 | a.False(o.contains("not-exists"))
25 | a.False(o.contains(""))
26 | }
27 |
28 | func TestOutput_Sanitize(t *testing.T) {
29 | a := assert.New(t, false)
30 |
31 | // 默认的 Type
32 | o := &Output{}
33 | a.NotError(o.sanitize())
34 | a.Equal(o.Type, APIDocXML).NotNil(o.marshal)
35 |
36 | o = &Output{Type: "invalid-type"}
37 | a.Error(o.sanitize())
38 |
39 | o = &Output{Type: APIDocXML}
40 | o.Path = "./testdir/apidoc.json"
41 | a.NotError(o.sanitize())
42 | a.Equal(o.Style, docs.StylesheetURL(core.OfficialURL)).
43 | Equal(2, len(o.procInst)).
44 | Contains(o.procInst[1], docs.StylesheetURL(core.OfficialURL))
45 |
46 | o.Version = "1.0.0"
47 | a.NotError(o.sanitize())
48 | o.Version = "1"
49 | a.Error(o.sanitize())
50 | }
51 |
52 | func TestOptions_buffer(t *testing.T) {
53 | a := assert.New(t, false)
54 |
55 | doc := asttest.Get()
56 | o := &Output{
57 | Type: OpenapiJSON,
58 | Path: "./openapi.json",
59 | }
60 | a.NotError(o.sanitize())
61 | _, err := o.buffer(doc)
62 | a.NotError(err)
63 |
64 | doc = asttest.Get()
65 | o = &Output{}
66 | a.NotError(o.sanitize())
67 | buf, err := o.buffer(doc)
68 | a.NotError(err).NotNil(buf)
69 | }
70 |
71 | func TestFilterDoc(t *testing.T) {
72 | a := assert.New(t, false)
73 |
74 | d := asttest.Get()
75 | o := &Output{}
76 | a.NotError(o.sanitize())
77 | filterDoc(d, o)
78 | a.Equal(3, len(d.Tags))
79 |
80 | d = asttest.Get()
81 | o = &Output{
82 | Tags: []string{"t1"},
83 | }
84 | a.NotError(o.sanitize())
85 | filterDoc(d, o)
86 | a.Equal(1, len(d.Tags)).
87 | Equal(2, len(d.APIs))
88 |
89 | d = asttest.Get()
90 | o = &Output{
91 | Tags: []string{"t1", "t2"},
92 | }
93 | a.NotError(o.sanitize())
94 | filterDoc(d, o)
95 | a.Equal(2, len(d.Tags)).
96 | Equal(2, len(d.APIs))
97 |
98 | d = asttest.Get()
99 | o = &Output{
100 | Tags: []string{"tag1"},
101 | }
102 | a.NotError(o.sanitize())
103 | filterDoc(d, o)
104 | a.Equal(1, len(d.Tags)).
105 | Equal(1, len(d.APIs))
106 |
107 | d = asttest.Get()
108 | o = &Output{
109 | Tags: []string{"not-exists"},
110 | }
111 | a.NotError(o.sanitize())
112 | filterDoc(d, o)
113 | a.Equal(0, len(d.Tags)).
114 | Equal(0, len(d.APIs))
115 | }
116 |
--------------------------------------------------------------------------------
/build/path.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package build
4 |
5 | import (
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | "github.com/caixw/apidoc/v7/internal/locale"
12 | )
13 |
14 | // 获取 path 的绝对路径
15 | //
16 | // 如果 path 是相对路径的,则将其设置为相对于 wd 的路径。
17 | func abs(path, wd core.URI) (uri core.URI, err error) {
18 | scheme, p := path.Parse()
19 | if scheme != "" && scheme != core.SchemeFile {
20 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme)
21 | }
22 |
23 | scheme, dir := wd.Parse()
24 | if scheme != "" && scheme != core.SchemeFile {
25 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme)
26 | }
27 | if !filepath.IsAbs(dir) {
28 | if dir, err = filepath.Abs(dir); err != nil {
29 | return "", err
30 | }
31 | }
32 |
33 | ps := string(p)
34 | switch {
35 | case ps == "":
36 | return core.FileURI(dir), nil
37 | case ps[0] == '~':
38 | dir, err := os.UserHomeDir()
39 | if err != nil {
40 | return "", err
41 | }
42 |
43 | return core.FileURI(dir).Append(ps[1:]), nil
44 | case filepath.IsAbs(ps) || ps[0] == '/' || ps[0] == os.PathSeparator:
45 | return core.FileURI(ps), nil
46 | default: // 相对路径
47 | return core.FileURI(filepath.Clean(filepath.Join(dir, ps))), nil
48 | }
49 | }
50 |
51 | // 获取 path 相对于 wd 的路径
52 | //
53 | // 如果两者不存在关联性,则返回 path 的原始值。
54 | // 返回值仅为普通的路径表示,不会带 scheme 内容。
55 | func rel(path, wd core.URI) (uri core.URI, err error) {
56 | scheme, p := path.Parse()
57 | if scheme != "" && scheme != core.SchemeFile {
58 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme)
59 | }
60 | if !filepath.IsAbs(p) {
61 | if p, err = filepath.Abs(p); err != nil {
62 | return "", err
63 | }
64 | }
65 |
66 | scheme, dir := wd.Parse()
67 | if scheme != "" && scheme != core.SchemeFile {
68 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme)
69 | }
70 | if !filepath.IsAbs(dir) {
71 | if dir, err = filepath.Abs(dir); err != nil {
72 | return "", err
73 | }
74 | }
75 |
76 | pp, err := filepath.Rel(dir, p)
77 | if err != nil || strings.HasPrefix(pp, "../") || strings.HasPrefix(pp, "..\\") {
78 | return path, nil
79 | }
80 | return core.URI(pp), nil
81 | }
82 |
--------------------------------------------------------------------------------
/build/path_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package build
4 |
5 | import (
6 | "os"
7 | "runtime"
8 | "testing"
9 |
10 | "github.com/issue9/assert/v3"
11 |
12 | "github.com/caixw/apidoc/v7/core"
13 | )
14 |
15 | type pathTester struct {
16 | path, wd, result string
17 | }
18 |
19 | func TestAbs(t *testing.T) {
20 | if runtime.GOOS == "windows" { // windows 由 path_windows_test.go 程序测试
21 | return
22 | }
23 |
24 | a := assert.New(t, false)
25 | hd, err := os.UserHomeDir()
26 | a.NotError(err).NotNil(hd)
27 | hdURI := core.FileURI(hd)
28 | a.NotEmpty(hdURI)
29 |
30 | data := []*pathTester{
31 | {
32 | path: "",
33 | wd: "file:///wd/",
34 | result: "file:///wd/",
35 | },
36 | {
37 | path: "~/path",
38 | wd: "file:///wd/",
39 | result: hdURI.Append("/path").String(),
40 | },
41 | {
42 | path: "/path",
43 | wd: "file:///wd/",
44 | result: "file:///path",
45 | },
46 | {
47 | path: "file:///path",
48 | wd: "file:///wd/",
49 | result: "file:///path",
50 | },
51 | {
52 | path: "path",
53 | wd: "file:///wd/",
54 | result: "file:///wd/path",
55 | },
56 | {
57 | path: "../path",
58 | wd: "file:///wd/",
59 | result: "file:///path",
60 | },
61 | {
62 | path: "../../path",
63 | wd: "file:///wd/dir",
64 | result: "file:///path",
65 | },
66 | {
67 | path: "./path",
68 | wd: "file:///wd/",
69 | result: "file:///wd/path",
70 | },
71 | {
72 | path: "path",
73 | wd: "/wd/",
74 | result: "file:///wd/path",
75 | },
76 | {
77 | path: "./path",
78 | wd: "/wd/",
79 | result: "file:///wd/path",
80 | },
81 | }
82 |
83 | for index, item := range data {
84 | result, err := abs(core.URI(item.path), core.URI(item.wd))
85 |
86 | a.NotError(err, "err @%d,%s", index, err).
87 | Equal(result, item.result, "not equal @%d,v1=%s,v2=%s", index, result, item.result)
88 | }
89 | }
90 |
91 | func TestRel(t *testing.T) {
92 | if runtime.GOOS == "windows" { // windows 由 path_windows_test.go 程序测试
93 | return
94 | }
95 |
96 | a := assert.New(t, false)
97 | hd, err := os.UserHomeDir()
98 | a.NotError(err).NotNil(hd)
99 | hdURI := core.FileURI(hd)
100 | a.NotEmpty(hdURI)
101 |
102 | data := []*pathTester{
103 | {
104 | path: "",
105 | wd: "file:///wd/",
106 | result: "",
107 | },
108 | {
109 | path: "/wd/path",
110 | wd: "file:///wd/",
111 | result: "path",
112 | },
113 | {
114 | path: "/wd/path",
115 | wd: "file:///wd1/",
116 | result: "/wd/path",
117 | },
118 | {
119 | path: "wd/path",
120 | wd: "file:///wd/",
121 | result: "wd/path",
122 | },
123 | {
124 | path: "path",
125 | wd: "file:///wd/",
126 | result: "path",
127 | },
128 | {
129 | path: "path",
130 | wd: "/wd/",
131 | result: "path",
132 | },
133 | {
134 | path: "file:///wd/path",
135 | wd: "/wd/",
136 | result: "path",
137 | },
138 | }
139 |
140 | for index, item := range data {
141 | result, err := rel(core.URI(item.path), core.URI(item.wd))
142 |
143 | a.NotError(err, "err @%d,%s", index, err).
144 | Equal(result, item.result, "not equal @%d,v1=%s,v2=%s", index, result, item.result)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/build/testdata/failed.yaml:
--------------------------------------------------------------------------------
1 | # 这是一个无效的 yaml 文件,用于测试
2 |
3 | version: 1.1.x
4 | : value
5 |
--------------------------------------------------------------------------------
/build/testdata/gbk.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/gbk.php
--------------------------------------------------------------------------------
/build/testdata/no-extension:
--------------------------------------------------------------------------------
1 | 表示没有扩展名的文件
2 |
--------------------------------------------------------------------------------
/build/testdata/testdir1/testfile.1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/testdir1/testfile.1
--------------------------------------------------------------------------------
/build/testdata/testdir1/testfile.2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/testdir1/testfile.2
--------------------------------------------------------------------------------
/build/testdata/testdir2/testfile.1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/testdir2/testfile.1
--------------------------------------------------------------------------------
/build/testdata/testfile.1:
--------------------------------------------------------------------------------
1 | // 这里展示的是一段没有结尾符号的 C 风格注释代码
2 |
3 | /*
4 | *
5 | * desc
6 | * test
7 | *
8 |
--------------------------------------------------------------------------------
/build/testdata/testfile.c:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //
4 | //
5 | //
6 | // test
7 | //
8 | void api() {
9 | // api
10 | }
11 |
--------------------------------------------------------------------------------
/build/testdata/testfile.h:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //
4 | // title
5 | // application/json
6 | //
7 | //
8 |
--------------------------------------------------------------------------------
/cmd/apidoc/.gitignore:
--------------------------------------------------------------------------------
1 | apidoc
2 |
--------------------------------------------------------------------------------
/cmd/apidoc/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // apidoc 是一个 RESTful API 文档生成工具
4 | //
5 | // 大致的使用方法为:
6 | //
7 | // apidoc cmd [args]
8 | //
9 | // 其中的 cmd 为子命令,args 代码传递给该子命令的参数。
10 | // 可以使用 help 查看每个子命令的具体说明:
11 | //
12 | // apidoc help [cmd]
13 | package main
14 |
15 | import (
16 | "fmt"
17 | "os"
18 |
19 | "github.com/issue9/localeutil"
20 | "golang.org/x/text/language"
21 |
22 | "github.com/caixw/apidoc/v7"
23 | "github.com/caixw/apidoc/v7/internal/cmd"
24 | "github.com/caixw/apidoc/v7/internal/locale"
25 | )
26 |
27 | func main() {
28 | tag, err := localeutil.DetectUserLanguageTag()
29 | if err != nil { // 无法获取系统语言,则采用默认值
30 | fmt.Fprintln(os.Stderr, err, tag)
31 | tag = language.MustParse(locale.DefaultLocaleID)
32 | }
33 | apidoc.SetLocale(tag)
34 |
35 | if err := cmd.Init(os.Stdout).Exec(os.Args[1:]); err != nil {
36 | if _, err := fmt.Fprintln(os.Stderr, err); err != nil {
37 | panic(err)
38 | }
39 | os.Exit(2)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/core/core.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // Package core 提供基础的核心功能
4 | package core
5 |
6 | import "fmt"
7 |
8 | const (
9 | // Name 程序的正式名称
10 | Name = "apidoc"
11 |
12 | // RepoURL 源码仓库地址
13 | RepoURL = "https://github.com/caixw/apidoc"
14 |
15 | // OfficialURL 官网
16 | OfficialURL = "https://apidoc.tools"
17 |
18 | // XMLNamespace 定义 xml 命名空间的 URI
19 | XMLNamespace = "https://apidoc.tools/v6/XMLSchema"
20 | )
21 |
22 | // Searcher 实现了搜索的基本方法集合
23 | //
24 | // 所有内嵌 [Location] 的对象都可以使用此接口判断是否内嵌 [Location]。
25 | type Searcher interface {
26 | Contains(URI, Position) bool
27 | Loc() Location
28 | }
29 |
30 | // Block 最基本的代码单位
31 | //
32 | // 一般从注释提取的一个完整注释作为 Block 实例。
33 | type Block struct {
34 | Location Location
35 | Data []byte
36 | }
37 |
38 | // Position 用于描述字符在文件中的定位
39 | //
40 | // 兼容 LSP https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
41 | type Position struct {
42 | Line int `json:"line" apidoc:"-"`
43 | Character int `json:"character" apidoc:"-"`
44 | }
45 |
46 | // Range 用于描述文档中的一段范围
47 | //
48 | // 兼容 LSP https://microsoft.github.io/language-server-protocol/specifications/specification-current/#range
49 | type Range struct {
50 | Start Position `json:"start" apidoc:"-"`
51 | End Position `json:"end" apidoc:"-"`
52 | }
53 |
54 | // Location 用于描述一段内容的定位
55 | //
56 | // 兼容 LSP https://microsoft.github.io/language-server-protocol/specifications/specification-current/#location
57 | type Location struct {
58 | URI URI `json:"uri" apidoc:"-"`
59 | Range Range `json:"range" apidoc:"-"`
60 | }
61 |
62 | // Equal 判断与 v 是否相同
63 | func (p Position) Equal(v Position) bool {
64 | return p.Line == v.Line && p.Character == v.Character
65 | }
66 |
67 | // Equal 判断与 v 是否相同
68 | func (r Range) Equal(v Range) bool {
69 | return r.Start.Equal(v.Start) && r.End.Equal(v.End)
70 | }
71 |
72 | // IsEmpty 表示 Range 表示的范围长度为空
73 | func (r Range) IsEmpty() bool { return r.End == r.Start }
74 |
75 | // Contains 是否包含了 p 这个点
76 | func (r Range) Contains(p Position) bool {
77 | s := r.Start
78 | e := r.End
79 | return (s.Line < p.Line || (s.Line == p.Line && s.Character <= p.Character)) &&
80 | (e.Line > p.Line || (e.Line == p.Line && e.Character >= p.Character))
81 | }
82 |
83 | // Loc 返回当前的范围
84 | func (l Location) Loc() Location { return l }
85 |
86 | // Contains l 是否包含 pos 这个点
87 | func (l Location) Contains(uri URI, pos Position) bool {
88 | return l.URI == uri && l.Range.Contains(pos)
89 | }
90 |
91 | func (l Location) String() string {
92 | if l.IsEmpty() {
93 | return ""
94 | }
95 |
96 | if l.Range.IsEmpty() {
97 | return l.URI.String()
98 | }
99 |
100 | s := l.Range.Start
101 | e := l.Range.End
102 | return fmt.Sprintf("%s[%d:%d,%d:%d]", l.URI, s.Line, s.Character, e.Line, e.Character)
103 | }
104 |
105 | // Equal 判断与 v 是否相等
106 | //
107 | // 所有字段都相同即返回 true。
108 | func (l Location) Equal(v Location) bool {
109 | return l.Range.Equal(v.Range) && l.URI == v.URI
110 | }
111 |
112 | // IsEmpty 表示 Location 未指向任何位置
113 | func (l Location) IsEmpty() bool {
114 | return l.URI == "" && l.Range.IsEmpty()
115 | }
116 |
--------------------------------------------------------------------------------
/core/errors_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package core
4 |
5 | import (
6 | "errors"
7 | "os"
8 | "testing"
9 |
10 | "github.com/issue9/assert/v3"
11 |
12 | "github.com/caixw/apidoc/v7/internal/locale"
13 | )
14 |
15 | var _ error = &Error{}
16 |
17 | func TestError(t *testing.T) {
18 | a := assert.New(t, false)
19 |
20 | err1 := NewError("msg")
21 | err2 := NewError("msg").WithField("field")
22 | a.NotEqual(err1.Error(), err2.Error())
23 | }
24 |
25 | func TestWithError(t *testing.T) {
26 | a := assert.New(t, false)
27 |
28 | err := errors.New("test")
29 | serr := WithError(err).WithField("field")
30 | a.Equal(serr.Err, err)
31 |
32 | serr2 := WithError(serr).WithLocation(Location{URI: "uri"})
33 | a.Equal(serr2.Err, err)
34 | }
35 |
36 | func TestError_AddTypes(t *testing.T) {
37 | a := assert.New(t, false)
38 | loc := Location{}
39 |
40 | err := loc.WithError(errors.New("err1"))
41 | err.AddTypes(ErrorTypeDeprecated)
42 | a.Equal(err.Types, []ErrorType{ErrorTypeDeprecated})
43 | err.AddTypes(ErrorTypeDeprecated)
44 | a.Equal(err.Types, []ErrorType{ErrorTypeDeprecated})
45 |
46 | err.AddTypes(ErrorTypeUnused)
47 | a.Equal(err.Types, []ErrorType{ErrorTypeDeprecated, ErrorTypeUnused})
48 | }
49 |
50 | func TestError_Is_Unwrap(t *testing.T) {
51 | a := assert.New(t, false)
52 |
53 | err := WithError(os.ErrExist).WithField("field")
54 | a.True(errors.Is(err, os.ErrExist))
55 |
56 | a.Equal(errors.Unwrap(err), os.ErrExist)
57 | }
58 |
59 | func TestError_Relate(t *testing.T) {
60 | a := assert.New(t, false)
61 |
62 | err := NewError(locale.ErrInvalidUTF8Character)
63 | a.Empty(err.Related)
64 | err.Relate(Location{}, "msg")
65 | a.Equal(1, len(err.Related)).Equal(err.Related[0].Message, "msg")
66 |
67 | err.Relate(Location{}, "msg2")
68 | a.Equal(2, len(err.Related)).Equal(err.Related[1].Message, "msg2")
69 | }
70 |
--------------------------------------------------------------------------------
/core/gbk.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/core/gbk.php
--------------------------------------------------------------------------------
/core/message.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package core
4 |
5 | import (
6 | "golang.org/x/text/message"
7 |
8 | "github.com/caixw/apidoc/v7/internal/locale"
9 | )
10 |
11 | // MessageType 表示消息的类型
12 | type MessageType int8
13 |
14 | // 消息的分类
15 | const (
16 | Erro MessageType = iota
17 | Warn
18 | Info
19 | Succ
20 | )
21 |
22 | func (t MessageType) String() string {
23 | switch t {
24 | case Erro:
25 | return "ERRO"
26 | case Warn:
27 | return "WARN"
28 | case Info:
29 | return "INFO"
30 | case Succ:
31 | return "SUCC"
32 | default:
33 | return ""
34 | }
35 | }
36 |
37 | // Message 输出消息的具体结构
38 | type Message struct {
39 | Type MessageType
40 | Message any
41 | }
42 |
43 | // HandlerFunc 错误处理函数
44 | type HandlerFunc func(*Message)
45 |
46 | // MessageHandler 异步的消息处理机制
47 | //
48 | // 包含了本地化的信息,输出时,会以指定的本地化内容输出
49 | type MessageHandler struct {
50 | messages chan *Message
51 | stop chan struct{}
52 | }
53 |
54 | // NewMessageHandler 声明新的 MessageHandler 实例
55 | func NewMessageHandler(f HandlerFunc) *MessageHandler {
56 | h := &MessageHandler{
57 | messages: make(chan *Message, 100),
58 | stop: make(chan struct{}),
59 | }
60 |
61 | go func() {
62 | for msg := range h.messages {
63 | f(msg)
64 | }
65 | h.stop <- struct{}{}
66 | }()
67 |
68 | return h
69 | }
70 |
71 | // Stop 停止处理错误内容
72 | //
73 | // 只有在消息处理完成之后,才会返回。
74 | func (h *MessageHandler) Stop() {
75 | close(h.messages)
76 |
77 | // Stop() 调用可能是在主程序结束处。
78 | // 通过 h.stop 阻塞函数返回,直到所有消息都处理完成。
79 | <-h.stop
80 | }
81 |
82 | // Message 发送消息
83 | func (h *MessageHandler) Message(t MessageType, msg any) {
84 | h.messages <- &Message{
85 | Type: t,
86 | Message: msg,
87 | }
88 | }
89 |
90 | // Locale 发送普通的文本信息
91 | func (h *MessageHandler) Locale(t MessageType, key message.Reference, val ...any) {
92 | h.Message(t, locale.New(key, val...))
93 | }
94 |
95 | // Error 发送错误类型的值
96 | func (h *MessageHandler) Error(err any) { h.Message(Erro, err) }
97 |
98 | // Warning 发送错误类型的值
99 | func (h *MessageHandler) Warning(err any) { h.Message(Warn, err) }
100 |
101 | // Success 发送错误类型的值
102 | func (h *MessageHandler) Success(err any) { h.Message(Succ, err) }
103 |
104 | // Info 发送错误类型的值
105 | func (h *MessageHandler) Info(err any) { h.Message(Info, err) }
106 |
--------------------------------------------------------------------------------
/core/message_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package core
4 |
5 | import (
6 | "bytes"
7 | "fmt"
8 | "testing"
9 | "time"
10 |
11 | "github.com/issue9/assert/v3"
12 |
13 | "github.com/caixw/apidoc/v7/internal/locale"
14 | )
15 |
16 | var _ fmt.Stringer = Erro
17 |
18 | func TestType_String(t *testing.T) {
19 | a := assert.New(t, false)
20 | a.Equal("ERRO", Erro.String())
21 | a.Equal("SUCC", Succ.String())
22 | a.Equal("INFO", Info.String())
23 | a.Equal("WARN", Warn.String())
24 | a.Equal("", MessageType(-22).String())
25 | }
26 |
27 | func TestHandler(t *testing.T) {
28 | a := assert.New(t, false)
29 |
30 | erro := new(bytes.Buffer)
31 | warn := new(bytes.Buffer)
32 | info := new(bytes.Buffer)
33 | succ := new(bytes.Buffer)
34 | h := NewMessageHandler(func(msg *Message) {
35 | switch msg.Type {
36 | case Erro:
37 | erro.WriteString("erro")
38 | case Warn:
39 | warn.WriteString("warn")
40 | case Info:
41 | info.WriteString("info")
42 | case Succ:
43 | succ.WriteString("succ")
44 | default:
45 | panic("panic")
46 | }
47 | })
48 | a.NotNil(h)
49 |
50 | h.Error((Location{URI: "erro.go"}).NewError(locale.ErrInvalidUTF8Character))
51 | h.Warning((Location{URI: "warn.go"}).NewError(locale.ErrInvalidUTF8Character))
52 | h.Info((Location{URI: "info.go"}).NewError(locale.ErrInvalidUTF8Character))
53 | h.Success((Location{URI: "succ.go"}).NewError(locale.ErrInvalidUTF8Character))
54 |
55 | time.Sleep(1 * time.Second) // 等待 channel 完成
56 | a.Equal(erro.String(), "erro")
57 | a.Equal(warn.String(), "warn")
58 | a.Equal(info.String(), "info")
59 | a.Equal(succ.String(), "succ")
60 |
61 | h.Stop()
62 | a.Panic(func() { // 已经关闭 messages
63 | h.Error((Location{URI: "erro"}).NewError(locale.ErrInvalidUTF8Character))
64 | })
65 | }
66 |
67 | func TestHandler_Stop(t *testing.T) {
68 | a := assert.New(t, false)
69 | var exit bool
70 |
71 | h := NewMessageHandler(func(msg *Message) {
72 | time.Sleep(time.Second)
73 | exit = true
74 | })
75 | a.NotNil(h)
76 |
77 | h.Locale(Erro, locale.ErrInvalidUTF8Character)
78 | h.Stop() // 此处会阻塞,等待完成
79 | a.True(exit)
80 | }
81 |
--------------------------------------------------------------------------------
/core/messagetest/messagetest.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // Package messagetest 提供测试生成 message 相关的测试工具
4 | package messagetest
5 |
6 | import "github.com/caixw/apidoc/v7/core"
7 |
8 | type Result struct {
9 | Errors, Warns, Infos, Successes []any
10 | Handler *core.MessageHandler
11 | }
12 |
13 | // NewMessageHandler 返回一个用于测试的 core.MessageHandler 实例
14 | func NewMessageHandler() *Result {
15 | rslt := &Result{
16 | Errors: []any{},
17 | Warns: []any{},
18 | Infos: []any{},
19 | Successes: []any{},
20 | }
21 |
22 | rslt.Handler = core.NewMessageHandler(func(msg *core.Message) {
23 | switch msg.Type {
24 | case core.Erro:
25 | rslt.Errors = append(rslt.Errors, msg.Message)
26 | case core.Warn:
27 | rslt.Warns = append(rslt.Warns, msg.Message)
28 | case core.Info:
29 | rslt.Infos = append(rslt.Infos, msg.Message)
30 | case core.Succ:
31 | rslt.Successes = append(rslt.Successes, msg.Message)
32 | default:
33 | panic("unreached")
34 | }
35 | })
36 |
37 | return rslt
38 | }
39 |
--------------------------------------------------------------------------------
/core/messagetest/messagetest_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package messagetest
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | )
10 |
11 | func TestNewMessageHandler(t *testing.T) {
12 | a := assert.New(t, false)
13 |
14 | rslt := NewMessageHandler()
15 | a.NotNil(rslt).NotNil(rslt.Handler)
16 | rslt.Handler.Error("error")
17 | rslt.Handler.Stop()
18 | a.Equal(rslt.Errors[0], "error")
19 |
20 | rslt = NewMessageHandler()
21 | a.NotNil(rslt).NotNil(rslt.Handler)
22 | rslt.Handler.Info("info")
23 | rslt.Handler.Stop()
24 | a.Equal(rslt.Infos[0], "info")
25 |
26 | rslt = NewMessageHandler()
27 | a.NotNil(rslt).NotNil(rslt.Handler)
28 | rslt.Handler.Success("success")
29 | rslt.Handler.Stop()
30 | a.Equal(rslt.Successes[0], "success")
31 |
32 | rslt = NewMessageHandler()
33 | a.NotNil(rslt).NotNil(rslt.Handler)
34 | rslt.Handler.Warning("warn")
35 | rslt.Handler.Stop()
36 | a.Equal(rslt.Warns[0], "warn")
37 | }
38 |
--------------------------------------------------------------------------------
/core/version.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package core
4 |
5 | var (
6 | mainVersion = "7.2.3"
7 | metadata string
8 | fullVersion = mainVersion
9 | )
10 |
11 | func init() {
12 | if metadata != "" {
13 | fullVersion += "+" + metadata
14 | }
15 | }
16 |
17 | // FullVersion 完整的版本号
18 | //
19 | // 会包含版本号、构建日期和最后的提交 ID,大致格式如下:
20 | //
21 | // version+buildDate.commitHash
22 | func FullVersion() string {
23 | return fullVersion
24 | }
25 |
26 | // Version 程序的版本号
27 | //
28 | // 遵守 https://semver.org/lang/zh-CN/ 规则。
29 | // 程序不兼容或是文档格式不兼容时,需要提升主版本号。
30 | func Version() string {
31 | return mainVersion
32 | }
33 |
--------------------------------------------------------------------------------
/core/version_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package core
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | "github.com/issue9/version"
10 | )
11 |
12 | // 对一些堂量的基本检测。
13 | func TestVersion(t *testing.T) {
14 | a := assert.New(t, false)
15 |
16 | a.True(version.SemVerValid(Version()))
17 | a.True(version.SemVerValid(FullVersion()))
18 | }
19 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | apidoc.tools
2 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # docs
2 |
3 | 当前目录存放的是官网的内容。
4 |
5 | v5、v6 这些以版本名称命名的则是各个文档版本对应的转换工具。
6 |
--------------------------------------------------------------------------------
/docs/docs.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package docs
4 |
5 | import "embed"
6 |
7 | //go:embed *
8 | var FS embed.FS
9 |
--------------------------------------------------------------------------------
/docs/example/.apidoc.yaml:
--------------------------------------------------------------------------------
1 | version: 6.1.0
2 | inputs:
3 | - lang: c++
4 | dir: .
5 | recursive: true
6 |
7 | - lang: rust
8 | dir: .
9 | recursive: true
10 |
11 | output:
12 | path: ./index.xml
13 | type: apidoc+xml
14 | style: ../v6/apidoc.xsl
15 |
--------------------------------------------------------------------------------
/docs/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 |
3 | !.apidoc.yaml
4 |
--------------------------------------------------------------------------------
/docs/example/apis.cpp:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //
4 | // client
5 | // admin
6 | //
7 | // 这是关于接口的详细说明文档
9 | // 可以是一个 HTML 内容
10 | // ]]>
11 | //
12 | //
13 | //
14 | //
15 | //
16 | //
17 | //
18 | //
19 | //
20 | //
21 | //
22 | //
23 | //
24 | //
25 | //
26 | //
27 | //
28 | //
29 | //
30 | //
39 | //
40 | //
41 | //
42 | //
43 | // 这是一个回调函数的详细说明
45 | // 为一个 html 文档
46 | // ]]>
47 | //
48 | //
49 | //
50 | //
51 | //
52 | //
58 | //
59 | //
60 | //
61 | //
62 | //
63 | //
64 | void logs() {}
65 |
--------------------------------------------------------------------------------
/docs/example/doc.cpp:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //
4 | // 示例文档
5 | //
6 | //
admin
8 | // ]]>
9 | //
10 | //
11 | //
12 | // application/xml
13 | // application/json
14 | //
15 | //
16 | //
17 | //
18 | // https://example.com
19 | // example@example.com
20 | //
21 | //
22 | //
23 | //
24 | //
25 | //
26 | //
27 | //
28 | //
29 | //
30 | //
31 | //
32 | //
33 | //
34 | //
35 | //
36 | // 这是一个用于测试的文档用例
38 | // 状态码:
39 | //
40 | // - 40300:xxx
41 | // - 40301:xxx
42 | //
43 | // ]]>
44 | //
45 | //
46 |
--------------------------------------------------------------------------------
/docs/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/docs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | window.onload = function() {
4 | initGotoTop();
5 | };
6 |
7 | function initGotoTop() {
8 | const top = document.querySelector('.goto-top');
9 |
10 | // 在最顶部时,隐藏按钮
11 | window.addEventListener('scroll', (e) => {
12 | const body = document.querySelector('html');
13 | if (body.scrollTop > 50) {
14 | top.style.display = 'block';
15 | } else {
16 | top.style.display = 'none';
17 | }
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/docs/site.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | apidoc
7 | 6.1.0
8 | https://github.com/caixw/apidoc
9 | https://apidoc.tools
10 |
11 | C#
12 | C/C++
13 | D
14 | Dart
15 | Erlang
16 | Go
17 | Groovy
18 | Java
19 | JavaScript
20 | Julia
21 | Kotlin
22 | Lisp/Clojure
23 | Lua
24 | Nim
25 | Pascal/Delphi
26 | Perl
27 | PHP
28 | Python
29 | Ruby
30 | Rust
31 | Scala
32 | Swift
33 | TypeScript
34 | Zig
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/docs/v5/view.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
33 |
34 |
35 |
36 |
40 |
41 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/docs/v6/view.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
33 |
34 |
35 |
36 |
40 |
41 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/caixw/apidoc/v7
2 |
3 | require (
4 | github.com/issue9/assert/v3 v3.0.1
5 | github.com/issue9/cmdopt v0.7.3
6 | github.com/issue9/errwrap v0.3.1
7 | github.com/issue9/jsonrpc v0.12.2
8 | github.com/issue9/localeutil v0.12.3
9 | github.com/issue9/mux/v7 v7.0.0-beta.2
10 | github.com/issue9/qheader v0.6.2
11 | github.com/issue9/rands v1.2.1
12 | github.com/issue9/sliceutil v0.11.0
13 | github.com/issue9/source v0.2.0
14 | github.com/issue9/term/v3 v3.0.1
15 | github.com/issue9/validation v0.8.0
16 | github.com/issue9/version v1.0.6
17 | golang.org/x/text v0.3.7
18 | gopkg.in/yaml.v3 v3.0.1
19 | )
20 |
21 | require (
22 | github.com/gorilla/websocket v1.5.0 // indirect
23 | github.com/issue9/autoinc v1.0.9 // indirect
24 | github.com/issue9/unique v1.3.2 // indirect
25 | golang.org/x/sys v0.1.0 // indirect
26 | )
27 |
28 | go 1.18
29 |
--------------------------------------------------------------------------------
/internal/ast/ast_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package ast
4 |
5 | import (
6 | "strconv"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 | "github.com/issue9/version"
11 | )
12 |
13 | func TestVersion(t *testing.T) {
14 | a := assert.New(t, false)
15 | a.True(version.SemVerValid(Version))
16 |
17 | v := &version.SemVersion{}
18 | a.NotError(version.Parse(v, Version))
19 | major, err := strconv.Atoi(MajorVersion[1:])
20 | a.NotError(err)
21 | a.Equal(major, v.Major)
22 | }
23 |
24 | func TestParseType(t *testing.T) {
25 | a := assert.New(t, false)
26 |
27 | p, s := ParseType(TypeString)
28 | a.Equal(p, TypeString).Empty(s)
29 |
30 | p, s = ParseType(TypeURL)
31 | a.Equal(p, TypeString).Equal(s, "url")
32 |
33 | p, s = ParseType(TypeInt)
34 | a.Equal(p, TypeNumber).Equal(s, "int")
35 | }
36 |
37 | func TestTrimLeftSpace(t *testing.T) {
38 | a := assert.New(t, false)
39 |
40 | data := []*struct {
41 | input, output string
42 | }{
43 | {},
44 | {
45 | input: `abc`,
46 | output: `abc`,
47 | },
48 | {
49 | input: ` abc`,
50 | output: `abc`,
51 | },
52 | {
53 | input: " abc\n",
54 | output: "abc\n",
55 | },
56 | { // 缩进一个空格
57 | input: " abc\n abc\n",
58 | output: " abc\nabc\n",
59 | },
60 | { // 缩进一个空格
61 | input: "\n abc\n abc\n",
62 | output: "\n abc\nabc\n",
63 | },
64 | { // 缩进格式不相同,不会有缩进
65 | input: "\t abc\n abc\n",
66 | output: "\t abc\n abc\n",
67 | },
68 |
69 | {
70 | input: "\t abc\n\t abc\n\t xx\n",
71 | output: " abc\nabc\nxx\n",
72 | },
73 | {
74 | input: "\t abc\n\t abc\nxx\n",
75 | output: "\t abc\n\t abc\nxx\n",
76 | },
77 |
78 | { // 包含相同的 \t 内容
79 | input: "\t abc\n\t abc\n\t xx\n",
80 | output: "abc\nabc\nxx\n",
81 | },
82 |
83 | { // 部分空格相同
84 | input: "\t\t abc\n\t abc\n\t xx\n",
85 | output: "\t abc\n abc\n xx\n",
86 | },
87 | }
88 |
89 | for i, item := range data {
90 | output := trimLeftSpace(item.input)
91 | a.Equal(output, item.output, "not equal @ %d\nv1=%#v\nv2=%#v\n", i, output, item.output)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/internal/ast/asttest/asttest_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package asttest
4 |
5 | import (
6 | "path/filepath"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 | )
11 |
12 | func TestXML(t *testing.T) {
13 | a := assert.New(t, false)
14 | data := XML(a)
15 | a.NotNil(data)
16 | }
17 |
18 | func TestURI(t *testing.T) {
19 | a := assert.New(t, false)
20 |
21 | p1, err := filepath.Abs(Filename)
22 | a.NotError(err).NotEmpty(p1)
23 |
24 | p2, err := URI(a).File()
25 | a.NotError(err).NotEmpty(p2)
26 |
27 | a.Equal(p1, p2)
28 | }
29 |
30 | func TestPath(t *testing.T) {
31 | a := assert.New(t, false)
32 |
33 | p1, err := filepath.Abs(Filename)
34 | a.NotError(err).NotEmpty(p1)
35 |
36 | p2, err := filepath.Abs(Path(a))
37 | a.NotError(err).NotEmpty(p2)
38 |
39 | a.Equal(p1, p2)
40 | }
41 |
42 | func TestDir(t *testing.T) {
43 | a := assert.New(t, false)
44 |
45 | p1, err := filepath.Abs("./")
46 | a.NotError(err).NotEmpty(p1)
47 |
48 | p2, err := filepath.Abs(Dir(a))
49 | a.NotError(err).NotEmpty(p2)
50 |
51 | a.Equal(p1, p2)
52 | }
53 |
--------------------------------------------------------------------------------
/internal/ast/asttest/gen.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //go:generate go run ./make.go
4 |
5 | package asttest
6 |
--------------------------------------------------------------------------------
/internal/ast/asttest/index.xml:
--------------------------------------------------------------------------------
1 |
2 | test
3 | desc]]>
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | desc]]>
21 |
22 | t1
23 | t2
24 | admin
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | desc]]>
36 |
37 | t1
38 | tag1
39 | admin
40 | client
41 |
42 | application/json
43 | application/xml
44 |
--------------------------------------------------------------------------------
/internal/ast/asttest/make.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //go:build ignore
4 | // +build ignore
5 |
6 | package main
7 |
8 | import (
9 | "io/ioutil"
10 | "os"
11 |
12 | "github.com/caixw/apidoc/v7/core"
13 | "github.com/caixw/apidoc/v7/internal/ast/asttest"
14 | "github.com/caixw/apidoc/v7/internal/xmlenc"
15 | )
16 |
17 | func main() {
18 | data, err := xmlenc.Encode("\t", asttest.Get(), core.XMLNamespace, "aa")
19 | if err != nil {
20 | panic(err)
21 | }
22 |
23 | err = ioutil.WriteFile(asttest.Filename, data, os.ModePerm)
24 | if err != nil {
25 | panic(err)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/internal/ast/parse.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package ast
4 |
5 | import (
6 | "bytes"
7 | "errors"
8 | "io"
9 | "sort"
10 |
11 | "github.com/caixw/apidoc/v7/core"
12 | "github.com/caixw/apidoc/v7/internal/locale"
13 | "github.com/caixw/apidoc/v7/internal/xmlenc"
14 | )
15 |
16 | // ParseBlocks 从多个 core.Block 实例中解析文档内容
17 | //
18 | // g 必须是一个阻塞函数,直到所有代码块都写入参数之后,才能返回。
19 | func (doc *APIDoc) ParseBlocks(h *core.MessageHandler, g func(chan core.Block)) {
20 | done := make(chan struct{})
21 | blocks := make(chan core.Block, 50)
22 |
23 | go func() {
24 | for block := range blocks {
25 | doc.Parse(h, block)
26 | }
27 | done <- struct{}{}
28 | }()
29 |
30 | g(blocks)
31 | close(blocks)
32 | <-done
33 | }
34 |
35 | // Parse 将注释块的内容添加到当前文档
36 | func (doc *APIDoc) Parse(h *core.MessageHandler, b core.Block) {
37 | if !isValid(b) {
38 | return
39 | }
40 |
41 | p, err := xmlenc.NewParser(h, b)
42 | if err != nil {
43 | h.Error(err)
44 | return
45 | }
46 |
47 | switch getTagName(p) {
48 | case "api":
49 | if doc.APIs == nil {
50 | doc.APIs = make([]*API, 0, 100)
51 | }
52 |
53 | api := &API{doc: doc}
54 | xmlenc.Decode(p, api, core.XMLNamespace)
55 | doc.APIs = append(doc.APIs, api)
56 |
57 | if doc.Title.V() != "" { // apidoc 已经初始化,检测依赖于 apidoc 的字段
58 | api.sanitizeTags(p)
59 | }
60 | case "apidoc":
61 | if doc.Title != nil { // 多个 apidoc 标签
62 | err := b.Location.NewError(locale.ErrDuplicateValue).WithField("apidoc").
63 | Relate(doc.Location, locale.Sprintf(locale.ErrDuplicateValue))
64 | h.Error(err)
65 | return
66 | }
67 | xmlenc.Decode(p, doc, core.XMLNamespace)
68 | default:
69 | return
70 | }
71 |
72 | // api 进入 doc 的顺序是未知的,进行排序可以保证文档的顺序一致。
73 | doc.sortAPIs()
74 | }
75 |
76 | // 简单预判是否是一个合规的 apidoc 内容
77 | func isValid(b core.Block) bool {
78 | bs := bytes.TrimSpace(b.Data)
79 | if len(bs) < minSize {
80 | return false
81 | }
82 |
83 | // 去除空格之后,必须保证以 < 开头,且不能以 开关。
84 | return bs[0] == '<' && bs[1] != '/'
85 | }
86 |
87 | // 获取根标签的名称
88 | func getTagName(p *xmlenc.Parser) string {
89 | start := p.Current()
90 | for {
91 | t, _, err := p.Token()
92 | if errors.Is(err, io.EOF) {
93 | return ""
94 | } else if err != nil { // 获取第一个元素名称就出错,说明不是一个合则的 XML,直接忽略。
95 | return ""
96 | }
97 |
98 | switch elem := t.(type) {
99 | case *xmlenc.StartElement:
100 | p.Move(start)
101 | return elem.Name.Local.Value
102 | case *xmlenc.EndElement, *xmlenc.CData: // 表示是一个非法的 XML,忽略!
103 | return ""
104 | default: // 其它标签忽略
105 | }
106 | }
107 | }
108 |
109 | func (doc *APIDoc) sortAPIs() {
110 | sort.SliceStable(doc.APIs, func(i, j int) bool {
111 | ii := doc.APIs[i]
112 | jj := doc.APIs[j]
113 |
114 | var iip string
115 | if ii.Path != nil && ii.Path.Path != nil {
116 | iip = ii.Path.Path.V()
117 | }
118 |
119 | var jjp string
120 | if jj.Path != nil && jj.Path.Path != nil {
121 | jjp = jj.Path.Path.V()
122 | }
123 |
124 | var iim string
125 | if ii.Method != nil {
126 | iim = ii.Method.V()
127 | }
128 |
129 | var jjm string
130 | if jj.Method != nil {
131 | jjm = jj.Method.V()
132 | }
133 |
134 | if iip == jjp {
135 | return iim < jjm
136 | }
137 | return iip < jjp
138 | })
139 | }
140 |
--------------------------------------------------------------------------------
/internal/ast/search.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package ast
4 |
5 | import (
6 | "reflect"
7 | "unicode"
8 |
9 | "github.com/caixw/apidoc/v7/core"
10 | "github.com/caixw/apidoc/v7/internal/node"
11 | )
12 |
13 | var searcherType = reflect.TypeOf((*core.Searcher)(nil)).Elem()
14 |
15 | // Search 搜索符合条件的对象并返回
16 | //
17 | // 从 doc 中查找符合符合 pos 定位的最小对象,且该对象必须实现了 t 类型。
18 | // 如果不存在则返回 nil。t 必须是一个接口。
19 | func (doc *APIDoc) Search(uri core.URI, pos core.Position, t reflect.Type) (r core.Searcher) {
20 | r = search(reflect.ValueOf(doc), uri, pos, t)
21 | if r == nil { // apidoc 的 uri 可以与 api 的 uri 不同
22 | for _, api := range doc.APIs {
23 | if rr := search(reflect.ValueOf(api), uri, pos, t); rr != nil {
24 | return rr
25 | }
26 | }
27 | }
28 |
29 | return r
30 | }
31 |
32 | func search(v reflect.Value, uri core.URI, pos core.Position, t reflect.Type) (r core.Searcher) {
33 | if v.IsZero() {
34 | return
35 | }
36 |
37 | v = node.RealValue(v)
38 |
39 | if v.CanInterface() && v.Type().Implements(searcherType) && (t == nil || v.Type().Implements(t)) {
40 | if rr := v.Interface().(core.Searcher); rr.Contains(uri, pos) {
41 | r = rr
42 | }
43 | } else if v.CanAddr() {
44 | if pv := v.Addr(); pv.CanInterface() && pv.Type().Implements(searcherType) && (t == nil || pv.Type().Implements(t)) {
45 | if rr := pv.Interface().(core.Searcher); rr.Contains(uri, pos) {
46 | r = rr
47 | }
48 | }
49 | }
50 |
51 | if r == nil && t == nil { // 不匹配当前元素,也不需要搜查子元素是否实现 t,则直接返回 nil。
52 | return nil
53 | }
54 |
55 | if v.Kind() == reflect.Struct {
56 | for vt, i := v.Type(), 0; i < vt.NumField(); i++ {
57 | ft := vt.Field(i)
58 | if ft.Anonymous || unicode.IsLower(rune(ft.Name[0])) {
59 | continue
60 | }
61 |
62 | fv := v.Field(i)
63 | if fv.Kind() == reflect.Array || fv.Kind() == reflect.Slice {
64 | for j := 0; j < fv.Len(); j++ {
65 | if rr := search(fv.Index(j), uri, pos, t); rr != nil {
66 | return rr
67 | }
68 | }
69 | continue
70 | } else if rr := search(fv, uri, pos, t); rr != nil {
71 | return rr
72 | }
73 | }
74 | }
75 |
76 | return r
77 | }
78 |
--------------------------------------------------------------------------------
/internal/ast/testdata/api.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | tag1
11 | tag2
12 | admin
13 | client
14 |
15 |
16 | 这是描述信息
18 | ]]>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/internal/ast/testdata/doc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | title
5 |
6 |
7 | test@example.com
8 | https://example.com
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | client api
20 | ]]>
21 |
22 |
23 |
24 |
25 |
26 |
27 | application/xml
28 | application/json
29 |
30 |
31 | h2
33 | h3
34 | ]]>
35 |
36 |
37 |
--------------------------------------------------------------------------------
/internal/cmd/build.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "io"
7 | "time"
8 |
9 | "github.com/issue9/cmdopt"
10 |
11 | "github.com/caixw/apidoc/v7/build"
12 | "github.com/caixw/apidoc/v7/core"
13 | "github.com/caixw/apidoc/v7/internal/locale"
14 | )
15 |
16 | var buildDir = uri("./")
17 |
18 | func initBuild(command *cmdopt.CmdOpt) {
19 | fs := command.New("build", locale.Sprintf(locale.CmdBuildUsage), doBuild)
20 | fs.Var(&buildDir, "d", locale.Sprintf(locale.FlagBuildDirUsage))
21 | }
22 |
23 | func doBuild(io.Writer) error {
24 | start := time.Now()
25 |
26 | cfg, err := build.LoadConfig(core.URI(buildDir))
27 | if err != nil {
28 | return err
29 | }
30 |
31 | h := core.NewMessageHandler(messageHandle)
32 | defer h.Stop()
33 |
34 | cfg.Build(h)
35 | h.Locale(core.Info, locale.Complete, cfg.Output.Path, time.Since(start))
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/internal/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // Package cmd 提供子命令的相关功能
4 | package cmd
5 |
6 | import (
7 | "flag"
8 | "fmt"
9 | "io"
10 | "os"
11 |
12 | "github.com/issue9/cmdopt"
13 | "github.com/issue9/term/v3/colors"
14 | "golang.org/x/text/message"
15 |
16 | "github.com/caixw/apidoc/v7/core"
17 | "github.com/caixw/apidoc/v7/internal/locale"
18 | )
19 |
20 | // 命令行输出的表格中,每一列为了对齐填补的空格数量。
21 | const tail = 3
22 |
23 | var printers = map[core.MessageType]*printer{
24 | core.Erro: {
25 | out: os.Stderr,
26 | color: colors.Red,
27 | prefix: locale.ErrorPrefix,
28 | },
29 | core.Warn: {
30 | out: os.Stderr,
31 | color: colors.Cyan,
32 | prefix: locale.WarnPrefix,
33 | },
34 | core.Info: {
35 | out: os.Stdout,
36 | color: colors.Default,
37 | prefix: locale.InfoPrefix,
38 | },
39 | core.Succ: {
40 | out: os.Stdout,
41 | color: colors.Green,
42 | prefix: locale.SuccessPrefix,
43 | },
44 | }
45 |
46 | type printer struct {
47 | out io.Writer
48 | color colors.Color
49 | prefix message.Reference
50 | }
51 |
52 | type uri core.URI
53 |
54 | func (u uri) Get() any { return string(u) }
55 |
56 | func (u *uri) Set(v string) error {
57 | *u = uri(core.FileURI(v))
58 | return nil
59 | }
60 |
61 | func (u *uri) String() string { return core.URI(*u).String() }
62 |
63 | func (u uri) URI() core.URI { return core.URI(u) }
64 |
65 | // Init 初始化 cmdopt.CmdOpt 实例
66 | func Init(out io.Writer) *cmdopt.CmdOpt {
67 | command := &cmdopt.CmdOpt{
68 | Output: out,
69 | ErrorHandling: flag.ExitOnError,
70 | Header: locale.Sprintf(locale.CmdUsage, core.Name),
71 | Footer: locale.Sprintf(locale.CmdUsageFooter, core.OfficialURL, core.RepoURL),
72 | OptionsTitle: locale.Sprintf(locale.CmdUsageOptions),
73 | CommandsTitle: locale.Sprintf(locale.CmdUsageCommands),
74 | NotFound: func(name string) string {
75 | return locale.Sprintf(locale.CmdNotFound, name)
76 | },
77 | }
78 |
79 | command.Help("help", locale.Sprintf(locale.CmdHelpUsage))
80 | initBuild(command)
81 | initDetect(command)
82 | initLang(command)
83 | initLocale(command)
84 | initSyntax(command)
85 | initVersion(command)
86 | initMock(command)
87 | initStatic(command)
88 | initLSP(command)
89 |
90 | return command
91 | }
92 |
93 | func messageHandle(msg *core.Message) {
94 | printers[msg.Type].print(msg.Message)
95 | }
96 |
97 | func (p *printer) print(msg any) {
98 | if _, err := colors.Fprint(p.out, colors.Normal, p.color, colors.Default, locale.New(p.prefix)); err != nil {
99 | panic(err)
100 | }
101 |
102 | if _, err := fmt.Fprintln(p.out, msg); err != nil {
103 | panic(err)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/internal/cmd/cmd_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "bytes"
7 | "flag"
8 | "testing"
9 |
10 | "github.com/issue9/assert/v3"
11 | "github.com/issue9/term/v3/colors"
12 |
13 | "github.com/caixw/apidoc/v7/core"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | )
16 |
17 | var (
18 | uuu = uri("")
19 | _ flag.Getter = &uuu
20 | )
21 |
22 | func resetPrinters() (erro, warn, succ, info *bytes.Buffer) {
23 | erro = new(bytes.Buffer)
24 | warn = new(bytes.Buffer)
25 | succ = new(bytes.Buffer)
26 | info = new(bytes.Buffer)
27 |
28 | printers = map[core.MessageType]*printer{
29 | core.Erro: {
30 | out: erro,
31 | color: colors.Red,
32 | prefix: locale.ErrorPrefix,
33 | },
34 | core.Warn: {
35 | out: warn,
36 | color: colors.Cyan,
37 | prefix: locale.WarnPrefix,
38 | },
39 | core.Info: {
40 | out: info,
41 | color: colors.Default,
42 | prefix: locale.InfoPrefix,
43 | },
44 | core.Succ: {
45 | out: succ,
46 | color: colors.Green,
47 | prefix: locale.SuccessPrefix,
48 | },
49 | }
50 |
51 | return
52 | }
53 |
54 | func TestMessageHandle(t *testing.T) {
55 | a := assert.New(t, false)
56 |
57 | erro := new(bytes.Buffer)
58 | warn := new(bytes.Buffer)
59 | info := new(bytes.Buffer)
60 | succ := new(bytes.Buffer)
61 |
62 | printers[core.Erro].out = erro
63 | printers[core.Warn].out = warn
64 | printers[core.Info].out = info
65 | printers[core.Succ].out = succ
66 |
67 | h := core.NewMessageHandler(messageHandle)
68 | a.NotNil(h)
69 |
70 | h.Locale(core.Erro, "erro")
71 | h.Locale(core.Warn, "warn")
72 | h.Locale(core.Info, "info")
73 | h.Locale(core.Succ, "succ")
74 | h.Stop()
75 |
76 | a.Contains(erro.String(), "erro")
77 | a.Contains(warn.String(), "warn")
78 | a.Contains(info.String(), "info")
79 | a.Contains(succ.String(), "succ")
80 | }
81 |
--------------------------------------------------------------------------------
/internal/cmd/detect.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "fmt"
7 | "io"
8 |
9 | "github.com/issue9/cmdopt"
10 | "gopkg.in/yaml.v3"
11 |
12 | "github.com/caixw/apidoc/v7/build"
13 | "github.com/caixw/apidoc/v7/core"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | )
16 |
17 | var (
18 | detectRecursive bool
19 | detectWrite bool
20 | detectDir = uri("./")
21 | )
22 |
23 | func initDetect(command *cmdopt.CmdOpt) {
24 | fs := command.New("detect", locale.Sprintf(locale.CmdDetectUsage), detect)
25 | fs.BoolVar(&detectRecursive, "r", true, locale.Sprintf(locale.FlagDetectRecursiveUsage))
26 | fs.BoolVar(&detectWrite, "w", false, locale.Sprintf(locale.FlagDetectWrite))
27 | fs.Var(&buildDir, "d", locale.Sprintf(locale.FlagDetectDirUsage))
28 | }
29 |
30 | func detect(w io.Writer) error {
31 | h := core.NewMessageHandler(messageHandle)
32 | defer h.Stop()
33 |
34 | dir := detectDir.URI()
35 | cfg, err := build.DetectConfig(dir, detectRecursive)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | if !detectWrite {
41 | data, err := yaml.Marshal(cfg)
42 | if err != nil {
43 | return err
44 | }
45 | _, err = fmt.Fprint(w, string(data))
46 | return err
47 | }
48 |
49 | if err = cfg.Save(dir); err != nil {
50 | return err
51 | }
52 | h.Locale(core.Succ, locale.ConfigWriteSuccess, dir)
53 | return nil
54 | }
55 |
--------------------------------------------------------------------------------
/internal/cmd/detect_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "bytes"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 | "gopkg.in/yaml.v3"
11 |
12 | "github.com/caixw/apidoc/v7/build"
13 | "github.com/caixw/apidoc/v7/internal/ast"
14 | "github.com/caixw/apidoc/v7/internal/docs"
15 | )
16 |
17 | func TestCmdDetect(t *testing.T) {
18 | a := assert.New(t, false)
19 |
20 | buf := new(bytes.Buffer)
21 | path := docs.Dir().Append("example")
22 |
23 | cmd := Init(buf)
24 | resetPrinters()
25 | err := cmd.Exec([]string{"detect", "-d", path.String()})
26 | a.NotError(err)
27 | cfg := &build.Config{}
28 | a.NotError(yaml.Unmarshal(buf.Bytes(), cfg))
29 | a.Equal(cfg.Version, ast.Version)
30 |
31 | cmd = Init(buf)
32 | resetPrinters()
33 | err = cmd.Exec([]string{"detect", "-d", path.String(), "-w"})
34 | a.NotError(err)
35 | cfg2 := &build.Config{}
36 | data, err := build.LoadConfig(path)
37 | a.NotError(err).NotNil(data)
38 | a.NotError(yaml.Unmarshal(buf.Bytes(), cfg2))
39 | a.Equal(cfg, cfg2)
40 | }
41 |
--------------------------------------------------------------------------------
/internal/cmd/lang.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "fmt"
7 | "io"
8 | "strings"
9 |
10 | "github.com/issue9/cmdopt"
11 | "golang.org/x/text/width"
12 |
13 | "github.com/caixw/apidoc/v7/internal/lang"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | )
16 |
17 | func initLang(command *cmdopt.CmdOpt) {
18 | command.New("lang", locale.Sprintf(locale.CmdLangUsage), doLang)
19 | }
20 |
21 | func doLang(w io.Writer) error {
22 | ls := lang.Langs()
23 | langs := make([]*lang.Language, 1, len(ls)+1)
24 | langs[0] = &lang.Language{
25 | ID: locale.Sprintf(locale.LangID),
26 | DisplayName: locale.Sprintf(locale.LangName),
27 | Exts: []string{locale.Sprintf(locale.LangExts)},
28 | }
29 | langs = append(langs, ls...)
30 |
31 | // 计算各列的最大长度值
32 | var maxName, maxID int
33 | for _, l := range langs {
34 | calcMaxWidth(l.DisplayName, &maxName)
35 | calcMaxWidth(l.ID, &maxID)
36 | }
37 | maxName += tail
38 | maxID += tail
39 |
40 | for _, l := range langs {
41 | id := l.ID + strings.Repeat(" ", maxID-textWidth(l.ID))
42 | name := l.DisplayName + strings.Repeat(" ", maxName-textWidth(l.DisplayName))
43 | if _, err := fmt.Fprintln(w, id, name, strings.Join(l.Exts, " ")); err != nil {
44 | return err
45 | }
46 | }
47 |
48 | return nil
49 | }
50 |
51 | func calcMaxWidth(content string, max *int) {
52 | if w := textWidth(content); w > *max {
53 | *max = w
54 | }
55 | }
56 |
57 | func textWidth(text string) (w int) {
58 | for _, r := range text {
59 | switch width.LookupRune(rune(r)).Kind() {
60 | case width.EastAsianFullwidth, width.EastAsianWide:
61 | w += 2
62 | default:
63 | w++
64 | }
65 | }
66 | return
67 | }
68 |
--------------------------------------------------------------------------------
/internal/cmd/lang_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "bufio"
7 | "bytes"
8 | "io"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/issue9/assert/v3"
13 |
14 | "github.com/caixw/apidoc/v7/internal/lang"
15 | )
16 |
17 | func TestDoLang(t *testing.T) {
18 | a := assert.New(t, false)
19 | w := new(bytes.Buffer)
20 |
21 | lines := func(w *bytes.Buffer) []string {
22 | b := bufio.NewReader(w)
23 | lines := make([]string, 0, 100)
24 | for line, err := b.ReadString('\n'); err != io.EOF; line, err = b.ReadString('\n') {
25 | lines = append(lines, line)
26 | }
27 |
28 | return lines
29 | }
30 |
31 | a.NotError(doLang(w))
32 | ls := lines(w)
33 | a.Equal(len(ls), len(lang.Langs())+1)
34 | for _, l := range ls {
35 | cnt := strings.Count(l, strings.Repeat(" ", tail))
36 | a.True(cnt >= 2) // 至少三列
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/internal/cmd/locale.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "fmt"
7 | "io"
8 | "strings"
9 |
10 | "github.com/issue9/cmdopt"
11 | "golang.org/x/text/language/display"
12 |
13 | "github.com/caixw/apidoc/v7"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | )
16 |
17 | func initLocale(command *cmdopt.CmdOpt) {
18 | command.New("locale", locale.Sprintf(locale.CmdLocaleUsage), doLocale)
19 | }
20 |
21 | func doLocale(w io.Writer) error {
22 | locales := make(map[string]string, len(apidoc.Locales()))
23 |
24 | // 计算各列的最大长度值
25 | var maxID int
26 | for _, tag := range apidoc.Locales() {
27 | id := tag.String()
28 | calcMaxWidth(id, &maxID)
29 | locales[id] = display.Self.Name(tag)
30 | }
31 | maxID += tail
32 |
33 | for k, v := range locales {
34 | id := k + strings.Repeat(" ", maxID-len(k))
35 | if _, err := fmt.Fprintln(w, id, v); err != nil {
36 | return err
37 | }
38 | }
39 |
40 | return nil
41 | }
42 |
--------------------------------------------------------------------------------
/internal/cmd/locale_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "bufio"
7 | "bytes"
8 | "io"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/issue9/assert/v3"
13 |
14 | "github.com/caixw/apidoc/v7"
15 | )
16 |
17 | func TestLocale(t *testing.T) {
18 | a := assert.New(t, false)
19 | w := new(bytes.Buffer)
20 |
21 | lines := func(w *bytes.Buffer) []string {
22 | b := bufio.NewReader(w)
23 | lines := make([]string, 0, 100)
24 | for line, err := b.ReadString('\n'); err != io.EOF; line, err = b.ReadString('\n') {
25 | lines = append(lines, line)
26 | }
27 |
28 | return lines
29 | }
30 |
31 | a.NotError(doLocale(w))
32 | ls := lines(w)
33 | a.Equal(len(ls), len(apidoc.Locales()))
34 | for _, l := range ls {
35 | cnt := strings.Count(l, strings.Repeat(" ", tail))
36 | a.True(cnt >= 1) // 至少两列
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/internal/cmd/lsp.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "io"
7 | "log"
8 | "time"
9 |
10 | "github.com/issue9/cmdopt"
11 |
12 | "github.com/caixw/apidoc/v7"
13 | "github.com/caixw/apidoc/v7/internal/locale"
14 | )
15 |
16 | var (
17 | lspPort string
18 | lspMode string
19 | lspHeader bool
20 | lspTimeout time.Duration
21 | )
22 |
23 | func initLSP(command *cmdopt.CmdOpt) {
24 | ls := command.New("lsp", locale.Sprintf(locale.CmdLSPUsage), doLSP)
25 | ls.StringVar(&lspPort, "p", ":8080", locale.Sprintf(locale.FlagLSPPortUsage))
26 | ls.StringVar(&lspMode, "m", "stdio", locale.Sprintf(locale.FlagLSPModeUsage))
27 | ls.BoolVar(&lspHeader, "h", false, locale.Sprintf(locale.FlagLSPHeaderUsage))
28 | ls.DurationVar(&lspTimeout, "t", time.Second, locale.Sprintf(locale.FlagLSPTimeoutUsage))
29 | }
30 |
31 | func doLSP(o io.Writer) error {
32 | return apidoc.ServeLSP(lspHeader, lspMode, lspPort, lspTimeout, log.New(o, "", 0), log.New(o, "", 0))
33 | }
34 |
--------------------------------------------------------------------------------
/internal/cmd/mock_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "flag"
7 | "testing"
8 | "time"
9 |
10 | "github.com/issue9/assert/v3"
11 | )
12 |
13 | var (
14 | _ flag.Getter = servers{}
15 | _ flag.Getter = &slice{}
16 | _ flag.Getter = &size{}
17 | _ flag.Getter = &dateRange{}
18 | )
19 |
20 | func TestServers_Set(t *testing.T) {
21 | a := assert.New(t, false)
22 |
23 | srv := servers{}
24 | a.Equal(srv, srv.Get())
25 | a.Equal(0, len(srv))
26 |
27 | a.Error(srv.Set(""))
28 | a.Equal(0, len(srv))
29 | a.Equal(srv.String(), "")
30 |
31 | a.NotError(srv.Set("k1=v1"))
32 | a.Equal(1, len(srv))
33 | a.Equal(srv["k1"], "v1")
34 | a.NotEmpty(srv.String())
35 |
36 | a.NotError(srv.Set("k1=v1,k2=v2"))
37 | a.Equal(2, len(srv))
38 | a.Equal(srv["k1"], "v1")
39 | a.NotEmpty(srv.String())
40 |
41 | a.NotError(srv.Set("k1= v1, k2= v2"))
42 | a.Equal(2, len(srv))
43 | a.Equal(srv["k1"], " v1")
44 | a.Equal(srv["k2"], " v2")
45 | a.NotEmpty(srv.String())
46 | }
47 |
48 | func TestSize_Set(t *testing.T) {
49 | a := assert.New(t, false)
50 |
51 | s := &size{}
52 | a.Error(s.Set(""))
53 |
54 | a.Error(s.Set(","))
55 | a.Error(s.Set("1,"))
56 | a.Error(s.Set(",5"))
57 |
58 | a.NotError(s.Set("1,5"))
59 | a.Equal(s.Min, 1).Equal(s.Max, 5)
60 | }
61 |
62 | func TestDateRange_Set(t *testing.T) {
63 | a := assert.New(t, false)
64 |
65 | d := &dateRange{}
66 | a.Error(d.Set(""))
67 |
68 | a.Error(d.Set(","))
69 | a.Error(d.Set("2020-01-02T17:04:15+01:00,"))
70 | a.Error(d.Set(",2020-01-07T17:04:15+01:00"))
71 |
72 | a.NotError(d.Set("2020-01-02T17:04:15+01:00,2020-01-05T17:04:15+01:00"))
73 | start, err := time.Parse(time.RFC3339, "2020-01-02T17:04:15+01:00")
74 | a.NotError(err)
75 | end, err := time.Parse(time.RFC3339, "2020-01-05T17:04:15+01:00")
76 | a.NotError(err)
77 | a.Equal(d.start, start).Equal(d.end, end)
78 | }
79 |
80 | func TestSlice_Set(t *testing.T) {
81 | a := assert.New(t, false)
82 |
83 | s := &slice{}
84 | a.NotError(s.Set("")).Equal(1, len(*s))
85 | a.NotError(s.Set(",")).Equal(2, len(*s))
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/internal/cmd/static.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "io"
7 | "log"
8 | "net/http"
9 |
10 | "github.com/issue9/cmdopt"
11 |
12 | "github.com/caixw/apidoc/v7"
13 | "github.com/caixw/apidoc/v7/core"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | )
16 |
17 | var (
18 | staticPort string
19 | staticDocs uri
20 | staticStylesheet bool
21 | staticContentType string
22 | staticURL string
23 | staticPath uri
24 | )
25 |
26 | func initStatic(command *cmdopt.CmdOpt) {
27 | fs := command.New("static", locale.Sprintf(locale.CmdStaticUsage), static)
28 | fs.StringVar(&staticPort, "p", ":8080", locale.Sprintf(locale.FlagStaticPortUsage))
29 | fs.Var(&staticDocs, "docs", locale.Sprintf(locale.FlagStaticDocsUsage))
30 | fs.StringVar(&staticContentType, "ct", "", locale.Sprintf(locale.FlagStaticContentTypeUsage))
31 | fs.StringVar(&staticURL, "url", "", locale.Sprintf(locale.FlagStaticURLUsage))
32 | fs.BoolVar(&staticStylesheet, "stylesheet", false, locale.Sprintf(locale.FlagStaticStylesheetUsage))
33 | fs.Var(&staticPath, "path", locale.Sprintf(locale.FlagStaticPathUsage))
34 | }
35 |
36 | func static(io.Writer) (err error) {
37 | path := core.URI(staticPath)
38 | h := core.NewMessageHandler(messageHandle)
39 | defer h.Stop()
40 |
41 | var handler http.Handler
42 |
43 | if path == "" {
44 | handler = apidoc.Static(staticDocs.URI(), staticStylesheet, log.Default())
45 | } else {
46 |
47 | s := &apidoc.Server{
48 | Status: http.StatusOK,
49 | Path: staticURL,
50 | ContentType: staticContentType,
51 | Dir: staticDocs.URI(),
52 | Stylesheet: staticStylesheet,
53 | }
54 | handler, err = s.File(path)
55 | if err != nil {
56 | return err
57 | }
58 | }
59 |
60 | h.Locale(core.Succ, locale.ServerStart, staticPort)
61 |
62 | return http.ListenAndServe(staticPort, handler)
63 | }
64 |
--------------------------------------------------------------------------------
/internal/cmd/syntax.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "io"
7 |
8 | "github.com/issue9/cmdopt"
9 |
10 | "github.com/caixw/apidoc/v7/build"
11 | "github.com/caixw/apidoc/v7/core"
12 | "github.com/caixw/apidoc/v7/internal/locale"
13 | )
14 |
15 | var syntaxDir uri = uri(core.FileURI("./"))
16 |
17 | func initSyntax(command *cmdopt.CmdOpt) {
18 | fs := command.New("syntax", locale.Sprintf(locale.CmdSyntaxUsage), syntax)
19 | fs.Var(&syntaxDir, "d", locale.Sprintf(locale.FlagSyntaxDirUsage))
20 | }
21 |
22 | func syntax(w io.Writer) error {
23 | cfg, err := build.LoadConfig(syntaxDir.URI())
24 | if err != nil {
25 | return err
26 | }
27 |
28 | h := core.NewMessageHandler(messageHandle)
29 | defer h.Stop()
30 |
31 | cfg.CheckSyntax(h)
32 | h.Locale(core.Succ, locale.TestSuccess)
33 | return nil
34 | }
35 |
--------------------------------------------------------------------------------
/internal/cmd/syntax_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "bytes"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 |
11 | "github.com/caixw/apidoc/v7/internal/docs"
12 | )
13 |
14 | func TestCmdCheckSyntax(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | buf := new(bytes.Buffer)
18 | cmd := Init(buf)
19 | erro, _, succ, _ := resetPrinters()
20 | err := cmd.Exec([]string{"syntax", "-d", docs.Dir().Append("example").String()})
21 | a.NotError(err)
22 | a.Empty(buf.String()).
23 | Empty(erro.String()).
24 | NotEmpty(succ.String())
25 | }
26 |
--------------------------------------------------------------------------------
/internal/cmd/version.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "fmt"
7 | "io"
8 | "runtime"
9 | "strings"
10 |
11 | "github.com/issue9/cmdopt"
12 |
13 | "github.com/caixw/apidoc/v7"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | "github.com/caixw/apidoc/v7/internal/openapi"
16 | )
17 |
18 | var versionKind string
19 |
20 | func initVersion(command *cmdopt.CmdOpt) {
21 | fs := command.New("version", locale.Sprintf(locale.CmdVersionUsage), version)
22 | fs.StringVar(&versionKind, "kind", "all", locale.FlagVersionKindUsage)
23 | }
24 |
25 | func version(w io.Writer) error {
26 | var msg string
27 | switch strings.ToLower(versionKind) {
28 | case "doc":
29 | msg = apidoc.DocVersion
30 | case "lsp":
31 | msg = apidoc.LSPVersion
32 | case "openapi":
33 | msg = openapi.LatestVersion
34 | case "apidoc":
35 | msg = apidoc.Version(true)
36 | default: // all 与 default 采取相同的输出
37 | goVersion := strings.TrimLeft(runtime.Version(), "go")
38 | msg = locale.Sprintf(locale.Version, apidoc.Version(true), apidoc.DocVersion, apidoc.LSPVersion, openapi.LatestVersion, goVersion)
39 | }
40 | _, err := fmt.Fprintln(w, msg)
41 | return err
42 | }
43 |
--------------------------------------------------------------------------------
/internal/cmd/version_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package cmd
4 |
5 | import (
6 | "bytes"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 |
11 | "github.com/caixw/apidoc/v7"
12 | "github.com/caixw/apidoc/v7/internal/openapi"
13 | )
14 |
15 | func TestCmdVersion(t *testing.T) {
16 | a := assert.New(t, false)
17 |
18 | buf := new(bytes.Buffer)
19 | cmd := Init(buf)
20 | resetPrinters()
21 | a.NotError(cmd.Exec([]string{"version"}))
22 | a.Contains(buf.String(), apidoc.LSPVersion).
23 | Contains(buf.String(), apidoc.DocVersion).
24 | Contains(buf.String(), apidoc.Version(true))
25 |
26 | buf.Reset()
27 | cmd = Init(buf)
28 | resetPrinters()
29 | a.NotError(cmd.Exec([]string{"version", "-kind", "apidoc"}))
30 | a.Equal(buf.String(), apidoc.Version(true)+"\n")
31 |
32 | buf.Reset()
33 | cmd = Init(buf)
34 | resetPrinters()
35 | a.NotError(cmd.Exec([]string{"version", "-kind", "lsp"}))
36 | a.Equal(buf.String(), apidoc.LSPVersion+"\n")
37 |
38 | buf.Reset()
39 | cmd = Init(buf)
40 | resetPrinters()
41 | a.NotError(cmd.Exec([]string{"version", "-kind", "doc"}))
42 | a.Equal(buf.String(), apidoc.DocVersion+"\n")
43 |
44 | buf.Reset()
45 | cmd = Init(buf)
46 | resetPrinters()
47 | a.NotError(cmd.Exec([]string{"version", "-kind", "openapi"}))
48 | a.Equal(buf.String(), openapi.LatestVersion+"\n")
49 | }
50 |
--------------------------------------------------------------------------------
/internal/docs/gen.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //go:generate go run ./make_site.go
4 | //go:generate go run ../../cmd/apidoc/ build -d=../../docs/example
5 |
6 | package docs
7 |
--------------------------------------------------------------------------------
/internal/docs/make_site.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | //go:build ignore
4 | // +build ignore
5 |
6 | package main
7 |
8 | import (
9 | "github.com/caixw/apidoc/v7/internal/docs"
10 | "github.com/caixw/apidoc/v7/internal/docs/site"
11 | )
12 |
13 | func main() {
14 | if err := site.Write(docs.Dir()); err != nil {
15 | panic(err)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/internal/docs/site/command.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package site
4 |
5 | import (
6 | "bytes"
7 | "io"
8 |
9 | "github.com/caixw/apidoc/v7/internal/cmd"
10 | )
11 |
12 | func (d *doc) newCommands() error {
13 | out := new(bytes.Buffer)
14 | opt := cmd.Init(out)
15 | names := opt.Commands()
16 |
17 | for _, name := range names {
18 | out.Reset()
19 | if err := opt.Exec([]string{"help", name}); err != nil {
20 | return err
21 | }
22 |
23 | usage, err := out.ReadString('\n')
24 | if err != nil && err != io.EOF {
25 | return err
26 | }
27 |
28 | if usage[len(usage)-1] == '\n' { // 去掉换行符
29 | usage = usage[:len(usage)-1]
30 | }
31 | d.Commands = append(d.Commands, &command{
32 | Name: name,
33 | Usage: usage,
34 | })
35 | }
36 |
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/internal/docs/site/config.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package site
4 |
5 | import (
6 | "fmt"
7 | "reflect"
8 | "strings"
9 | "unicode"
10 |
11 | "github.com/caixw/apidoc/v7/build"
12 | "github.com/caixw/apidoc/v7/internal/locale"
13 | )
14 |
15 | func (d *doc) newConfig() error {
16 | return d.buildConfigObject("", reflect.TypeOf(build.Config{}))
17 | }
18 |
19 | func (d *doc) buildConfigItem(parent string, f reflect.StructField) error {
20 | name, omitempty := parseTag(f)
21 | if parent != "" {
22 | name = parent + "." + name
23 | }
24 |
25 | t := f.Type
26 | for t.Kind() == reflect.Ptr {
27 | t = t.Elem()
28 | }
29 |
30 | var array bool
31 | if t.Kind() == reflect.Array || t.Kind() == reflect.Slice {
32 | array = true
33 | t = t.Elem()
34 | for t.Kind() == reflect.Ptr {
35 | t = t.Elem()
36 | }
37 | }
38 |
39 | typeName := t.Kind().String()
40 | if t.Kind() == reflect.Struct {
41 | typeName = "object"
42 | }
43 |
44 | d.Config = append(d.Config, &item{
45 | Name: name,
46 | Type: typeName,
47 | Array: array,
48 | Required: !omitempty,
49 | Usage: locale.Sprintf("usage-config-" + name),
50 | })
51 |
52 | if isPrimitive(t) {
53 | return nil
54 | } else if t.Kind() != reflect.Struct {
55 | panic(fmt.Sprintf("字段 %s 的类型 %s 无法处理", f.Name, t.Kind()))
56 | }
57 |
58 | return d.buildConfigObject(name, t)
59 | }
60 |
61 | // 调用方需要保证 t.Kind() 为 reflect.Struct
62 | func (d *doc) buildConfigObject(parent string, t reflect.Type) error {
63 | for i := 0; i < t.NumField(); i++ {
64 | f := t.Field(i)
65 | if !unicode.IsUpper(rune(f.Name[0])) || f.Tag.Get("yaml") == "-" {
66 | continue
67 | }
68 | if err := d.buildConfigItem(parent, f); err != nil {
69 | return err
70 | }
71 | }
72 | return nil
73 | }
74 |
75 | func isPrimitive(t reflect.Type) bool {
76 | return t.Kind() == reflect.String || (t.Kind() >= reflect.Bool && t.Kind() <= reflect.Complex128)
77 | }
78 |
79 | func parseTag(f reflect.StructField) (string, bool) {
80 | tag := f.Tag.Get("yaml")
81 | if tag == "" {
82 | return f.Name, false
83 | }
84 |
85 | prop := strings.Split(tag, ",")
86 | if len(prop) == 1 {
87 | return getName(prop[0], f), false
88 | }
89 |
90 | return getName(prop[0], f), strings.TrimSpace(prop[1]) == "omitempty"
91 | }
92 |
93 | func getName(n string, f reflect.StructField) string {
94 | if n = strings.TrimSpace(n); n != "" {
95 | return n
96 | }
97 | return f.Name
98 | }
99 |
--------------------------------------------------------------------------------
/internal/docs/site/config_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package site
4 |
5 | import (
6 | "reflect"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 | )
11 |
12 | func TestParseTag(t *testing.T) {
13 | a := assert.New(t, false)
14 |
15 | name, omitempty := parseTag(reflect.StructField{Name: "F1"})
16 | a.Equal(name, "F1").False(omitempty)
17 |
18 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`apidoc:"xx"`)})
19 | a.Equal(name, "F1").False(omitempty)
20 |
21 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:"xx"`)})
22 | a.Equal(name, "xx").False(omitempty)
23 |
24 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:"xx,omitempty"`)})
25 | a.Equal(name, "xx").True(omitempty)
26 |
27 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:",omitempty"`)})
28 | a.Equal(name, "F1").True(omitempty)
29 |
30 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:"xx,omit"`)})
31 | a.Equal(name, "xx").False(omitempty)
32 | }
33 |
--------------------------------------------------------------------------------
/internal/docs/site/site_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package site
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/internal/lang"
11 | "github.com/caixw/apidoc/v7/internal/locale"
12 | )
13 |
14 | func TestGen(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | site, docs, err := gen()
18 | a.NotError(err).
19 | NotNil(site).
20 | NotNil(docs)
21 |
22 | a.Equal(len(site.Languages), len(lang.Langs())).
23 | Equal(len(site.Locales), len(locale.Tags()))
24 |
25 | a.Equal(len(docs), len(locale.Tags()))
26 |
27 | defLocale := docs[buildDocFilename(locale.DefaultLocaleID)]
28 | for _, cmd := range defLocale.Commands {
29 | if cmd.Name == "build" {
30 | a.Contains(locale.Translate(locale.DefaultLocaleID, locale.CmdBuildUsage), cmd.Usage)
31 | }
32 | }
33 |
34 | for _, item := range defLocale.Config {
35 | a.Equal(item.Usage, locale.Translate(locale.DefaultLocaleID, "usage-config-"+item.Name))
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/internal/docs/site/spec.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package site
4 |
5 | import (
6 | "reflect"
7 | "sort"
8 |
9 | "github.com/caixw/apidoc/v7/internal/locale"
10 | "github.com/caixw/apidoc/v7/internal/node"
11 | )
12 |
13 | func (d *doc) newSpec(v any) error {
14 | n := node.New("", reflect.ValueOf(v))
15 | if err := d.dumpToTypes(n); err != nil {
16 | return err
17 | }
18 |
19 | for _, t := range d.Spec {
20 | if len(t.Items) == 1 && t.Items[0].Name == "." {
21 | t.Items = nil
22 | }
23 | }
24 |
25 | sort.SliceStable(d.Spec, func(i, j int) bool {
26 | if len(d.Spec[i].Items) == 0 {
27 | return false
28 | }
29 | return len(d.Spec[j].Items) == 0
30 | })
31 |
32 | return nil
33 | }
34 |
35 | func (d *doc) dumpToTypes(n *node.Node) error {
36 | t := &spec{
37 | Name: n.TypeName,
38 | Usage: innerXML{Text: locale.Sprintf(n.Value.Usage)},
39 | Items: make([]*item, 0, len(n.Attributes)+len(n.Elements)),
40 | }
41 | d.Spec = append(d.Spec, t) // 保证子元素在后显示
42 |
43 | for _, attr := range n.Attributes {
44 | appendItem(t, "@"+attr.Name, attr.Value, attr.Usage, !attr.Omitempty)
45 |
46 | if nn := node.New(attr.Name, attr.Value); nn.TypeName != "" && !d.typeExists(nn.TypeName) {
47 | if err := d.dumpToTypes(nn); err != nil {
48 | return err
49 | }
50 | }
51 | }
52 |
53 | for _, elem := range n.Elements {
54 | appendItem(t, elem.Name, elem.Value, elem.Usage, !elem.Omitempty)
55 |
56 | typ := node.RealType(elem.Type())
57 | v := node.RealValue(elem.Value)
58 |
59 | if typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array {
60 | typ = node.RealType(typ.Elem())
61 | v = reflect.New(typ).Elem()
62 | }
63 |
64 | if nn := node.New(elem.Name, v); nn.TypeName != "" && !d.typeExists(nn.TypeName) {
65 | if err := d.dumpToTypes(nn); err != nil {
66 | return err
67 | }
68 | }
69 | }
70 |
71 | if n.CData != nil {
72 | appendItem(t, ".", n.CData.Value, n.CData.Usage, !n.CData.Omitempty)
73 | }
74 |
75 | if n.Content != nil {
76 | appendItem(t, ".", n.Content.Value, n.Content.Usage, !n.Content.Omitempty)
77 | }
78 |
79 | return nil
80 | }
81 |
82 | func appendItem(t *spec, name string, v reflect.Value, usageKey string, req bool) {
83 | var isSlice bool
84 | typ := node.RealValue(v).Type()
85 | for typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array {
86 | isSlice = true
87 | typ = typ.Elem()
88 | }
89 |
90 | tt := typ.Name()
91 | if vv := node.ParseValue(reflect.New(typ).Elem()); vv != nil {
92 | tt = vv.Name
93 | }
94 | t.Items = append(t.Items, &item{
95 | Name: name,
96 | Type: tt,
97 | Required: req,
98 | Array: isSlice,
99 | Usage: locale.Sprintf(usageKey),
100 | })
101 | }
102 |
103 | func (d *doc) typeExists(typeName string) bool {
104 | for _, t := range d.Spec {
105 | if t.Name == typeName {
106 | return true
107 | }
108 | }
109 | return false
110 | }
111 |
--------------------------------------------------------------------------------
/internal/docs/site/spec_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package site
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/internal/locale"
11 | )
12 |
13 | type (
14 | objectTag struct {
15 | RootName struct{} `apidoc:"apidoc,meta,usage-root"`
16 | ID intAttr `apidoc:"id,attr,usage"`
17 | Name stringTag `apidoc:"name,elem,usage"`
18 | }
19 |
20 | stringTag struct {
21 | Value string `apidoc:"-"`
22 | RootName struct{} `apidoc:"string,meta,usage-string"`
23 | }
24 |
25 | intAttr struct {
26 | Value int `apidoc:"-"`
27 | RootName struct{} `apidoc:"number,meta,usage-number"`
28 | }
29 | )
30 |
31 | func TestNewSpec(t *testing.T) {
32 | a := assert.New(t, false)
33 |
34 | ts := &doc{}
35 | err := ts.newSpec(&objectTag{})
36 | a.NotError(err)
37 | ts2 := &doc{Spec: []*spec{
38 | {
39 | Name: "apidoc",
40 | Usage: innerXML{Text: locale.Sprintf("usage-root")},
41 | Items: []*item{
42 | {
43 | Name: "@id",
44 | Usage: locale.Sprintf("usage"),
45 | Type: "number",
46 | Array: false,
47 | Required: true,
48 | },
49 | {
50 | Name: "name",
51 | Usage: locale.Sprintf("usage"),
52 | Type: "string",
53 | Array: false,
54 | Required: true,
55 | },
56 | },
57 | },
58 | {
59 | Name: "number",
60 | Usage: innerXML{Text: locale.Sprintf("usage-number")},
61 | Items: []*item{},
62 | },
63 | {
64 | Name: "string",
65 | Usage: innerXML{Text: locale.Sprintf("usage-string")},
66 | Items: []*item{},
67 | },
68 | }}
69 | a.Equal(ts.Spec, ts2.Spec, "not equal\nv1=%#v\nv2=%#v\n", ts.Spec, ts2.Spec)
70 | }
71 |
--------------------------------------------------------------------------------
/internal/lang/lang_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | import (
6 | "strings"
7 | "testing"
8 | "unicode"
9 |
10 | "github.com/issue9/assert/v3"
11 | )
12 |
13 | func TestLangs(t *testing.T) {
14 | a := assert.New(t, false)
15 |
16 | isLower := func(str string) bool {
17 | for _, r := range str {
18 | if unicode.IsUpper(r) {
19 | return false
20 | }
21 | }
22 | return true
23 | }
24 |
25 | // Langs() 返回的应该和 langs 有相同的长度
26 | a.Equal(len(Langs()), len(langs))
27 |
28 | for index, lang := range langs {
29 | a.NotEmpty(lang.ID, "语言名称不能为空,在 %d", index)
30 | a.True(isLower(lang.ID), "名称非小写 %s", lang.ID)
31 |
32 | // 检测 block
33 | a.NotEmpty(lang.blocks, "blocks 不能为空,在 %s", lang.ID)
34 |
35 | // 检测扩展名
36 | for _, ext := range lang.Exts {
37 | a.NotEmpty(ext, "空的扩展名在 %s", lang.ID).
38 | Equal(ext[0], '.', "扩展名 %s 必须以 . 开头在 %s", ext, lang.ID).
39 | Equal(strings.TrimSpace(ext), ext, "扩展名 %s 存在首尾空格", ext, lang.ID).
40 | True(isLower(ext), "非小写的扩展名 %s 在 %s", ext, lang.ID)
41 | }
42 | }
43 | }
44 |
45 | func TestGet(t *testing.T) {
46 | a := assert.New(t, false)
47 |
48 | l := Get("go")
49 | a.NotNil(l).
50 | Equal(l.ID, "go").
51 | Equal(l.Exts, []string{".go"})
52 |
53 | // 不比较大小写
54 | l = Get("Go")
55 | a.Nil(l)
56 | }
57 |
58 | func TestGetByExt(t *testing.T) {
59 | a := assert.New(t, false)
60 |
61 | l := GetByExt(".go")
62 | a.NotNil(l).Equal(l.ID, "go")
63 |
64 | l = GetByExt(".cxx")
65 | a.NotNil(l).Equal(l.ID, "c++")
66 |
67 | // 不存在
68 | l = GetByExt(".not-exists")
69 | a.Nil(l)
70 |
71 | a.Panic(func() {
72 | GetByExt("")
73 | })
74 |
75 | a.Panic(func() {
76 | GetByExt("go")
77 | })
78 | }
79 |
--------------------------------------------------------------------------------
/internal/lang/nim.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | import "bytes"
6 |
7 | type nimRawString struct {
8 | escape, begin1, begin2, end string
9 | }
10 |
11 | type nimMultipleString struct{}
12 |
13 | func newNimRawString() blocker {
14 | return &nimRawString{
15 | escape: `""`,
16 | begin1: `r"`,
17 | begin2: `R"`,
18 | end: `"`,
19 | }
20 | }
21 |
22 | func (s *nimRawString) beginFunc(l *parser) bool {
23 | return l.Match(s.begin1) || l.Match(s.begin2)
24 | }
25 |
26 | func (s *nimRawString) endFunc(l *parser) (data []byte, ok bool) {
27 | for {
28 | switch {
29 | case l.AtEOF():
30 | return nil, false
31 | case l.Match(s.escape): // 转义
32 | break
33 | case l.Match(s.end): // 结束
34 | return nil, true
35 | default:
36 | l.Next(1)
37 | }
38 | } // end for
39 | }
40 |
41 | func newNimMultipleString() blocker {
42 | return &nimMultipleString{}
43 | }
44 |
45 | func (s *nimMultipleString) beginFunc(l *parser) bool {
46 | return l.Match(`"""`)
47 | }
48 |
49 | func (s *nimMultipleString) endFunc(l *parser) ([]byte, bool) {
50 | for {
51 | pos := l.Current()
52 | switch {
53 | case l.AtEOF():
54 | return nil, false
55 | case l.Match(`"""`):
56 | if l.AtEOF() { // 后面没有内容了
57 | return nil, true
58 | }
59 |
60 | bs, ok := l.Delim('\n', true)
61 | if ok && len(bytes.TrimSpace(bs)) == 0 { // """ 后只有空格和回车符
62 | return nil, true
63 | }
64 | l.Move(pos)
65 | l.Next(1)
66 | default:
67 | l.Next(1)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/internal/lang/nim_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | "github.com/caixw/apidoc/v7/core/messagetest"
12 | )
13 |
14 | func TestNimRawString(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | b := newNimRawString()
18 | a.NotNil(b)
19 |
20 | rslt := messagetest.NewMessageHandler()
21 | l := newParser(rslt.Handler, core.Block{Data: []byte(`r"123""123"`)}, nil)
22 | a.NotNil(l)
23 | rslt.Handler.Stop()
24 | a.Empty(rslt.Errors).NotNil(l)
25 | a.True(b.beginFunc(l))
26 | data, ok := b.endFunc(l)
27 | a.True(ok).
28 | Equal(len(data), 0) // 不返回内容
29 | bs := l.Next(1) // 继续向后推进,才会
30 | a.Empty(bs).True(l.AtEOF()) // 到达末尾
31 |
32 | rslt = messagetest.NewMessageHandler()
33 | l = newParser(rslt.Handler, core.Block{Data: []byte(`R"123123"`)}, nil)
34 | a.NotNil(l)
35 | rslt.Handler.Stop()
36 | a.Empty(rslt.Errors).NotNil(l)
37 | a.True(b.beginFunc(l))
38 | data, ok = b.endFunc(l)
39 | a.True(ok).
40 | Empty(data) // 不返回内容
41 | bs = l.Next(1) // 继续向后推进,才会
42 | a.Empty(bs).True(l.AtEOF()) // 到达末尾
43 | }
44 |
45 | func TestNimMultipleString(t *testing.T) {
46 | a := assert.New(t, false)
47 |
48 | b := newNimMultipleString()
49 | a.NotNil(b)
50 |
51 | rslt := messagetest.NewMessageHandler()
52 | l := newParser(rslt.Handler, core.Block{Data: []byte(`"""line1
53 | a.NotNil(l)
54 | line2
55 | """`)}, nil)
56 | rslt.Handler.Stop()
57 | a.Empty(rslt.Errors).NotNil(l)
58 | a.True(b.beginFunc(l))
59 | data, ok := b.endFunc(l)
60 | a.True(ok).Nil(data)
61 |
62 | // 结束符后带空格
63 | rslt = messagetest.NewMessageHandler()
64 | l = newParser(rslt.Handler, core.Block{Data: []byte(`"""line1
65 | line2
66 | """
67 | `)}, nil)
68 | a.NotNil(l)
69 | rslt.Handler.Stop()
70 | a.Empty(rslt.Errors).NotNil(l)
71 | a.True(b.beginFunc(l))
72 | data, ok = b.endFunc(l)
73 | a.True(ok).Nil(data)
74 |
75 | // 结束符后带字符
76 | rslt = messagetest.NewMessageHandler()
77 | l = newParser(rslt.Handler, core.Block{Data: []byte(`"""line1
78 | line2
79 | """ suffix
80 | `)}, nil)
81 | a.NotNil(l)
82 | rslt.Handler.Stop()
83 | a.Empty(rslt.Errors).NotNil(l)
84 | a.True(b.beginFunc(l))
85 | data, ok = b.endFunc(l)
86 | a.False(ok).Nil(data)
87 | }
88 |
--------------------------------------------------------------------------------
/internal/lang/parse.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | import (
6 | "fmt"
7 |
8 | "github.com/caixw/apidoc/v7/core"
9 | "github.com/caixw/apidoc/v7/internal/lexer"
10 | "github.com/caixw/apidoc/v7/internal/locale"
11 | )
12 |
13 | // Parse 分析 data 的内容并输出到到 blocks
14 | func Parse(h *core.MessageHandler, langID string, data core.Block, blocks chan core.Block) {
15 | l := Get(langID)
16 | if l == nil {
17 | panic(fmt.Sprintf("%s 指定的语言解析器并不存在", langID))
18 | }
19 |
20 | if p := newParser(h, data, l.blocks); p != nil {
21 | p.parse(blocks)
22 | }
23 | }
24 |
25 | type parser struct {
26 | *lexer.Lexer
27 | blocks []blocker
28 | h *core.MessageHandler
29 | }
30 |
31 | func newParser(h *core.MessageHandler, block core.Block, blocks []blocker) *parser {
32 | l, err := lexer.New(block)
33 | if err != nil {
34 | h.Error(err)
35 | return nil
36 | }
37 |
38 | return &parser{
39 | Lexer: l,
40 | blocks: blocks,
41 | h: h,
42 | }
43 | }
44 |
45 | // 从当前位置往后查找,直到找到第一个与 blocks 中某个相匹配的,并返回该 Blocker 。
46 | func (l *parser) block() (blocker, core.Position) {
47 | for {
48 | if l.AtEOF() {
49 | return nil, core.Position{}
50 | }
51 |
52 | pos := l.Current()
53 | for _, block := range l.blocks {
54 | if block.beginFunc(l) {
55 | return block, pos.Position
56 | }
57 | }
58 |
59 | l.Next(1)
60 | }
61 | }
62 |
63 | // 分析 l.data 的内容并输出到 blocks
64 | func (l *parser) parse(blocks chan core.Block) {
65 | var block blocker
66 | var pos core.Position
67 | for {
68 | if l.AtEOF() {
69 | return
70 | }
71 |
72 | if block == nil {
73 | if block, pos = l.block(); block == nil { // 没有匹配的 block 了
74 | return
75 | }
76 | }
77 |
78 | data, ok := block.endFunc(l)
79 | if !ok { // 没有找到结束标签,那肯定是到文件尾了,可以直接返回。
80 | loc := core.Location{
81 | URI: l.Location.URI,
82 | Range: core.Range{
83 | Start: pos,
84 | End: l.Current().Position,
85 | },
86 | }
87 | l.h.Error(loc.NewError(locale.ErrNotFoundEndFlag))
88 | return
89 | }
90 |
91 | block = nil // 重置 block
92 |
93 | if len(data) == 0 {
94 | continue
95 | }
96 |
97 | blocks <- core.Block{
98 | Location: core.Location{
99 | URI: l.Location.URI,
100 | Range: core.Range{
101 | Start: pos,
102 | End: l.Current().Position,
103 | },
104 | },
105 | Data: data,
106 | }
107 | } // end for
108 | }
109 |
--------------------------------------------------------------------------------
/internal/lang/pascal.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | // 描述了 pascal/delphi 语言的字符串,在 pascal 中
6 | // 转义字符即引号本身,不适合直接在 block 中定义。
7 | type pascalStringBlock struct {
8 | symbol string
9 | escape string
10 | }
11 |
12 | func newPascalStringBlock(symbol byte) blocker {
13 | s := string(symbol)
14 | return &pascalStringBlock{
15 | symbol: s,
16 | escape: s + s,
17 | }
18 | }
19 |
20 | func (b *pascalStringBlock) beginFunc(l *parser) bool {
21 | return l.Match(b.symbol)
22 | }
23 |
24 | func (b *pascalStringBlock) endFunc(l *parser) (data []byte, ok bool) {
25 | for {
26 | switch {
27 | case l.AtEOF():
28 | return nil, false
29 | case l.Match(b.escape): // 转义
30 | break
31 | case l.Match(b.symbol): // 结束
32 | return nil, true
33 | default:
34 | l.Next(1)
35 | }
36 | } // end for
37 | }
38 |
--------------------------------------------------------------------------------
/internal/lang/pascal_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | "github.com/caixw/apidoc/v7/core/messagetest"
12 | )
13 |
14 | func TestPascalStringBlock(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | b := newPascalStringBlock('"')
18 | a.NotNil(b)
19 |
20 | rslt := messagetest.NewMessageHandler()
21 | l := newParser(rslt.Handler, core.Block{Data: []byte(`"123""123"`)}, nil)
22 | rslt.Handler.Stop()
23 | a.Empty(rslt.Errors).NotNil(l)
24 | a.True(b.beginFunc(l))
25 | data, ok := b.endFunc(l)
26 | a.True(ok).
27 | Equal(len(data), 0) // 不返回内容
28 | bs := l.Next(1) // 继续向后推进,才会
29 | a.Empty(bs).True(l.AtEOF()) // 到达末尾
30 |
31 | rslt = messagetest.NewMessageHandler()
32 | l = newParser(rslt.Handler, core.Block{Data: []byte(`"123"""123"`)}, nil)
33 | rslt.Handler.Stop()
34 | a.Empty(rslt.Errors).NotNil(l)
35 | a.True(b.beginFunc(l))
36 | data, ok = b.endFunc(l)
37 | a.True(ok).
38 | Equal(len(data), 0). // 不返回内容
39 | Equal(string(l.All()), "123\"") // 未到达末尾
40 | }
41 |
--------------------------------------------------------------------------------
/internal/lang/php.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | const (
6 | phpHerodoc int8 = iota + 1
7 | phpNowdoc
8 | )
9 |
10 | type phpDocBlock struct {
11 | token1 string
12 | token2 string
13 | doctype int8
14 | }
15 |
16 | // herodoc 和 nowdoc 的实现。
17 | //
18 | // http://php.net/manual/zh/language.types.string.php#language.types.string.syntax.heredoc
19 | func newPHPDocBlock() blocker {
20 | return &phpDocBlock{
21 | doctype: phpHerodoc,
22 | }
23 | }
24 |
25 | func (b *phpDocBlock) beginFunc(l *parser) bool {
26 | prev := l.Current()
27 |
28 | if !l.Match("<<<") {
29 | return false
30 | }
31 |
32 | token, found := l.Delim('\n', true)
33 | if !found || len(token) <= 1 { // <<< 之后直接是换行符,则应该退回 <<< 字符
34 | l.Move(prev)
35 | return false
36 | }
37 | token = token[:len(token)-1] // l.delim 会带上换行符,需要去掉
38 |
39 | if token[0] == '\'' && token[len(token)-1] == '\'' ||
40 | token[0] == '"' && token[len(token)-1] == '"' {
41 | b.doctype = phpNowdoc
42 | token = token[1 : len(token)-1]
43 | }
44 |
45 | b.token1 = "\n" + string(token) + "\n"
46 | b.token2 = "\n" + string(token) + ";\n"
47 |
48 | return true
49 | }
50 |
51 | func (b *phpDocBlock) endFunc(l *parser) (data []byte, ok bool) {
52 | for {
53 | switch {
54 | case l.AtEOF():
55 | return nil, false
56 | case l.Match(b.token1):
57 | return nil, true
58 | case l.Match(b.token2):
59 | return nil, true
60 | default:
61 | l.Next(1)
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/internal/lang/php_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lang
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | "github.com/caixw/apidoc/v7/core/messagetest"
12 | )
13 |
14 | func TestPHPDocBlock(t *testing.T) {
15 | a := assert.New(t, false)
16 | b := newPHPDocBlock()
17 | a.NotNil(b)
18 |
19 | // herodoc
20 | data := []byte(`<<
27 | 标题
28 | xml
29 | json
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | `
39 | blk := core.Block{Data: []byte(b), Location: core.Location{URI: "file:///test/doc.go"}}
40 | rslt := messagetest.NewMessageHandler()
41 | doc := &ast.APIDoc{}
42 | doc.Parse(rslt.Handler, blk)
43 | rslt.Handler.Stop()
44 | a.Empty(rslt.Errors)
45 |
46 | s.folders = []*folder{
47 | {
48 | WorkspaceFolder: protocol.WorkspaceFolder{Name: "test", URI: "file:///test"},
49 | doc: doc,
50 | },
51 | }
52 |
53 | h = &protocol.Hover{}
54 | err = s.textDocumentHover(false, &protocol.HoverParams{TextDocumentPositionParams: protocol.TextDocumentPositionParams{
55 | TextDocument: protocol.TextDocumentIdentifier{URI: "file:///test/doc.go"},
56 | Position: core.Position{Line: 1, Character: 1},
57 | }}, h)
58 | a.NotError(err)
59 | a.Equal(h.Range, core.Range{
60 | Start: core.Position{Line: 1, Character: 1},
61 | End: core.Position{Line: 1, Character: 18},
62 | })
63 | a.Equal(h.Contents.Value, locale.Sprintf("usage-apidoc-title"))
64 | }
65 |
--------------------------------------------------------------------------------
/internal/lsp/initialize_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lsp
4 |
5 | import (
6 | "io/ioutil"
7 | "log"
8 | "testing"
9 |
10 | "github.com/issue9/assert/v3"
11 | "golang.org/x/text/language"
12 |
13 | "github.com/caixw/apidoc/v7/core"
14 | "github.com/caixw/apidoc/v7/internal/locale"
15 | "github.com/caixw/apidoc/v7/internal/lsp/protocol"
16 | )
17 |
18 | func TestServer_initialize(t *testing.T) {
19 | a := assert.New(t, false)
20 | s := newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
21 | in := &protocol.InitializeParams{}
22 | out := &protocol.InitializeResult{}
23 | a.NotError(s.initialize(false, in, out))
24 | a.Equal(out.ServerInfo.Name, core.Name)
25 | a.Equal(s.clientParams, in).Equal(s.serverResult, out)
26 | a.Equal(s.state, serverInitializing)
27 |
28 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
29 | in = &protocol.InitializeParams{
30 | Capabilities: protocol.ClientCapabilities{Workspace: &protocol.WorkspaceClientCapabilities{
31 | WorkspaceFolders: true,
32 | }},
33 | InitializationOptions: &protocol.InitializationOptions{Locale: "cmn-hant"},
34 | }
35 | out = &protocol.InitializeResult{}
36 | a.NotError(s.initialize(false, in, out))
37 | a.Equal(locale.Tag(), language.MustParse("cmn-hant"))
38 | a.True(out.Capabilities.Workspace.WorkspaceFolders.Supported).
39 | True(out.Capabilities.Workspace.WorkspaceFolders.ChangeNotifications)
40 |
41 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
42 | in = &protocol.InitializeParams{
43 | Capabilities: protocol.ClientCapabilities{TextDocument: protocol.TextDocumentClientCapabilities{
44 | Hover: &protocol.HoverCapabilities{},
45 | FoldingRange: &protocol.FoldingRangeClientCapabilities{},
46 | Definition: &protocol.DefinitionClientCapabilities{},
47 | }},
48 | }
49 | out = &protocol.InitializeResult{}
50 | a.NotError(s.initialize(false, in, out))
51 | a.False(out.Capabilities.HoverProvider).
52 | True(out.Capabilities.DefinitionProvider).
53 | True(out.Capabilities.FoldingRangeProvider)
54 |
55 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
56 | in = &protocol.InitializeParams{
57 | Capabilities: protocol.ClientCapabilities{TextDocument: protocol.TextDocumentClientCapabilities{
58 | Hover: &protocol.HoverCapabilities{ContentFormat: []protocol.MarkupKind{protocol.MarkupKindMarkdown}},
59 | References: &protocol.ReferenceClientCapabilities{},
60 | Completion: &protocol.CompletionClientCapabilities{},
61 | }},
62 | }
63 | out = &protocol.InitializeResult{}
64 | a.NotError(s.initialize(false, in, out))
65 | a.True(out.Capabilities.HoverProvider).
66 | False(out.Capabilities.DefinitionProvider).
67 | False(out.Capabilities.FoldingRangeProvider).
68 | True(out.Capabilities.ReferencesProvider).
69 | NotNil(out.Capabilities.CompletionProvider)
70 | }
71 |
--------------------------------------------------------------------------------
/internal/lsp/lsp.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // Package lsp 提供 language server protocol 服务
4 | package lsp
5 |
6 | import (
7 | "log"
8 | "net"
9 | "os"
10 | "strings"
11 | "time"
12 |
13 | "github.com/issue9/jsonrpc"
14 |
15 | "github.com/caixw/apidoc/v7/core"
16 | "github.com/caixw/apidoc/v7/internal/locale"
17 | )
18 |
19 | // Version lsp 的版本
20 | const Version = "3.16.0"
21 |
22 | // Serve 执行 LSP 服务
23 | //
24 | // t 表示服务的类型,可以是 stdio、udp、tcp 和 unix。
25 | func Serve(header bool, t string, addr string, timeout time.Duration, infolog, errlog *log.Logger) error {
26 | switch strings.ToLower(t) {
27 | case "stdio":
28 | return serveStdio(header, infolog, errlog)
29 | case "udp":
30 | return serveUDP(header, addr, timeout, infolog, errlog)
31 | case "tcp", "unix":
32 | return serveTCP(header, t, addr, timeout, infolog, errlog)
33 | }
34 |
35 | return core.NewError(locale.ErrInvalidValue)
36 | }
37 |
38 | func serveStdio(header bool, infolog, errlog *log.Logger) error {
39 | return serve(jsonrpc.NewStreamTransport(header, os.Stdin, os.Stdout, nil), infolog, errlog)
40 | }
41 |
42 | func serveUDP(header bool, addr string, timeout time.Duration, infolog, errlog *log.Logger) error {
43 | t, err := jsonrpc.NewUDPServerTransport(header, addr, timeout)
44 | if err != nil {
45 | return err
46 | }
47 | return serve(t, infolog, errlog)
48 | }
49 |
50 | // t 可以是 tcp 和 unix
51 | func serveTCP(header bool, t string, addr string, timeout time.Duration, infolog, errlog *log.Logger) error {
52 | l, err := net.Listen(t, addr)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | for {
58 | conn, err := l.Accept()
59 | if err != nil {
60 | errlog.Println(err)
61 | continue
62 | }
63 | return serve(jsonrpc.NewSocketTransport(header, conn, timeout), infolog, errlog)
64 | }
65 | }
66 |
67 | func serve(t jsonrpc.Transport, infolog, errlog *log.Logger) error {
68 | return newServe(t, infolog, errlog).serve()
69 | }
70 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/apidoc_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "net/http"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 |
11 | "github.com/caixw/apidoc/v7/internal/ast"
12 | "github.com/caixw/apidoc/v7/internal/ast/asttest"
13 | "github.com/caixw/apidoc/v7/internal/xmlenc"
14 | )
15 |
16 | func TestBuildAPIDocOutline(t *testing.T) {
17 | a := assert.New(t, false)
18 | f := WorkspaceFolder{Name: "test"}
19 |
20 | doc := &ast.APIDoc{}
21 | outline := BuildAPIDocOutline(f, doc)
22 | a.Nil(outline)
23 |
24 | doc = asttest.Get()
25 | outline = BuildAPIDocOutline(f, doc)
26 | a.NotNil(outline)
27 |
28 | a.Equal(outline.Title, doc.Title.V())
29 | a.Equal(2, len(outline.APIs))
30 | }
31 |
32 | func TestAPIDocOutline_appendAPI(t *testing.T) {
33 | a := assert.New(t, false)
34 |
35 | outline := &APIDocOutline{
36 | APIs: []*API{},
37 | }
38 |
39 | // api.Path 为空,无法构建 API.Path 变量,忽略该值
40 | outline.appendAPI(&ast.API{})
41 | a.Equal(len(outline.APIs), 1)
42 | api := outline.APIs[0]
43 | a.Equal(api.Path, "?").Empty(api.Method)
44 |
45 | outline = &APIDocOutline{
46 | APIs: []*API{},
47 | }
48 | outline.appendAPI(&ast.API{Method: &ast.MethodAttribute{Value: xmlenc.String{Value: http.MethodDelete}}})
49 | a.Equal(len(outline.APIs), 1)
50 | api = outline.APIs[0]
51 | a.Equal(api.Path, "?").Equal(api.Method, http.MethodDelete)
52 | }
53 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/completion_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "encoding/json"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 | )
11 |
12 | func TestCompletionList_MarshalJSON(t *testing.T) {
13 | a := assert.New(t, false)
14 |
15 | list := &CompletionList{}
16 | data, err := json.Marshal(list)
17 | a.NotError(err).Equal(string(data), "null")
18 |
19 | list.IsIncomplete = true
20 | list.Items = make([]CompletionItem, 0)
21 | data, err = json.Marshal(list)
22 | a.NotError(err).Equal(string(data), "null")
23 |
24 | list.Items = append(list.Items, CompletionItem{})
25 | data, err = json.Marshal(list)
26 | a.NotError(err).Equal(string(data), `{"isIncomplete":true,"items":[{"label":""}]}`)
27 | }
28 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/diagnostic_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | "github.com/caixw/apidoc/v7/internal/locale"
12 | )
13 |
14 | func TestPublishDiagnosticParams_AppendDiagnostic(t *testing.T) {
15 | a := assert.New(t, false)
16 | p := NewPublishDiagnosticsParams(core.URI("test.go"))
17 | a.NotNil(p).
18 | Equal(p.URI, core.URI("test.go")).
19 | Empty(p.Diagnostics)
20 |
21 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Erro)
22 | a.Equal(1, len(p.Diagnostics))
23 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Warn)
24 | a.Equal(2, len(p.Diagnostics))
25 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Info)
26 | a.Equal(3, len(p.Diagnostics))
27 | // 忽略 core.Succ
28 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Succ)
29 | a.Equal(3, len(p.Diagnostics))
30 |
31 | a.Panic(func() {
32 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), 100)
33 | })
34 | }
35 |
36 | func TestBuildDiagnostic(t *testing.T) {
37 | a := assert.New(t, false)
38 |
39 | err := core.NewError(locale.ErrInvalidUTF8Character).WithLocation(core.Location{
40 | Range: core.Range{Start: core.Position{Line: 1}},
41 | })
42 | d := buildDiagnostic(err, DiagnosticSeverityWarning)
43 | a.Equal(d.Severity, DiagnosticSeverityWarning)
44 | a.Empty(d.Tags)
45 | a.Equal(d.Range.Start.Line, 1).
46 | Empty(d.RelatedInformation).
47 | Empty(d.Tags)
48 |
49 | err = err.AddTypes(core.ErrorTypeDeprecated, core.ErrorTypeUnused)
50 | d = buildDiagnostic(err, DiagnosticSeverityError)
51 | a.Equal(d.Severity, DiagnosticSeverityError)
52 | a.Equal(d.Tags, []DiagnosticTag{DiagnosticTagDeprecated, DiagnosticTagUnnecessary})
53 | a.Equal(d.Range.Start.Line, 1)
54 |
55 | err = err.Relate(core.Location{URI: "relate.go"}, "relate message")
56 | d = buildDiagnostic(err, DiagnosticSeverityError)
57 | a.Equal(d.Range.Start.Line, 1).
58 | Equal(1, len(d.RelatedInformation)).
59 | Equal(d.RelatedInformation[0].Location, core.Location{URI: "relate.go"})
60 | }
61 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/folding.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import "github.com/caixw/apidoc/v7/internal/xmlenc"
6 |
7 | // 代码关折叠块的种类
8 | const (
9 | FoldingRangeKindComment = "comment" // Folding range for a comment
10 | FoldingRangeKindImports = "imports" // Folding range for a imports or includes
11 | FoldingRangeKindRegion = "region" // Folding range for a region (e.g. `#region`)
12 | )
13 |
14 | // FoldingRangeClientCapabilities 定义客户对代码拆叠功能的支持情况
15 | type FoldingRangeClientCapabilities struct {
16 | // Whether implementation supports dynamic registration for folding range providers. If this is set to `true`
17 | // the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
18 | // return value for the corresponding server capability as well.
19 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"`
20 |
21 | // The maximum number of folding ranges that the client prefers to receive per document. The value serves as a
22 | // hint, servers are free to follow the limit.
23 | RangeLimit int `json:"rangeLimit,omitempty"`
24 |
25 | // If set, the client signals that it only supports folding complete lines. If set, client will
26 | // ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange.
27 | LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"`
28 | }
29 |
30 | // FoldingRangeParams 由用户传递的 textDocument/foldingRange 参数
31 | type FoldingRangeParams struct {
32 | WorkDoneProgressParams
33 | PartialResultParams
34 |
35 | // The text document.
36 | TextDocument TextDocumentIdentifier `json:"textDocument"`
37 | }
38 |
39 | // FoldingRange represents a folding range
40 | type FoldingRange struct {
41 | // The zero-based line number from where the folded range starts.
42 | StartLine int `json:"startLine"`
43 |
44 | // The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line.
45 | //
46 | // 0 是一个合法的值,所以只能采用指针类型表示空值。
47 | StartCharacter *int `json:"startCharacter,omitempty"`
48 |
49 | // The zero-based line number where the folded range ends.
50 | EndLine int `json:"endLine"`
51 |
52 | // The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line.
53 | //
54 | // 0 是一个合法的值,所以只能采用指针类型表示空值。
55 | EndCharacter *int `json:"endCharacter,omitempty"`
56 |
57 | // Describes the kind of the folding range such as `comment` or `region`. The kind
58 | // is used to categorize folding ranges and used by commands like 'Fold all comments'. See
59 | // [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds.
60 | Kind string `json:"kind,omitempty"`
61 | }
62 |
63 | // BuildFoldingRange 根据参数构建 FoldingRange 实例
64 | func BuildFoldingRange(base xmlenc.Base, lineFoldingOnly bool) FoldingRange {
65 | item := FoldingRange{
66 | StartLine: base.Location.Range.Start.Line,
67 | EndLine: base.Location.Range.End.Line,
68 | Kind: FoldingRangeKindComment,
69 | }
70 |
71 | if lineFoldingOnly {
72 | item.StartCharacter = &base.Location.Range.Start.Character
73 | item.EndCharacter = &base.Location.Range.End.Character
74 | }
75 |
76 | return item
77 | }
78 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/folding_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | "github.com/caixw/apidoc/v7/internal/xmlenc"
12 | )
13 |
14 | func TestBuildFoldingRange(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | base := xmlenc.Base{}
18 | a.Equal(BuildFoldingRange(base, false), FoldingRange{Kind: FoldingRangeKindComment})
19 |
20 | base = xmlenc.Base{
21 | Location: core.Location{
22 | Range: core.Range{
23 | Start: core.Position{Line: 1, Character: 11},
24 | End: core.Position{Line: 2, Character: 11},
25 | },
26 | },
27 | }
28 | a.Equal(BuildFoldingRange(base, false), FoldingRange{
29 | StartLine: 1,
30 | Kind: FoldingRangeKindComment,
31 | EndLine: 2,
32 | })
33 | a.Equal(BuildFoldingRange(base, true), FoldingRange{
34 | StartLine: 1,
35 | StartCharacter: &base.Location.Range.Start.Character,
36 | EndLine: 2,
37 | EndCharacter: &base.Location.Range.End.Character,
38 | Kind: FoldingRangeKindComment,
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/hover.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "encoding/json"
7 |
8 | "github.com/caixw/apidoc/v7/core"
9 | )
10 |
11 | // HoverCapabilities 客户端有关 hover 功能的描述
12 | type HoverCapabilities struct {
13 | // Whether hover supports dynamic registration.
14 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"`
15 |
16 | // The client supports the follow content formats for the content
17 | // property. The order describes the preferred format of the client.
18 | ContentFormat []MarkupKind `json:"contentFormat,omitempty"`
19 | }
20 |
21 | // HoverParams textDocument/hover 发送的参数
22 | type HoverParams struct {
23 | WorkDoneProgressParams
24 | TextDocumentPositionParams
25 | }
26 |
27 | // Hover textDocument/hover 的返回结果
28 | type Hover struct {
29 | // The hover's content
30 | Contents MarkupContent `json:"contents"`
31 |
32 | // An optional range is a range inside a text document
33 | // that is used to visualize a hover, e.g. by changing the background color.
34 | Range core.Range `json:"range"`
35 | }
36 |
37 | // MarshalJSON 允许在 hover 为空值是返回 null
38 | func (h *Hover) MarshalJSON() ([]byte, error) {
39 | if h.Contents.Kind == "" {
40 | return json.Marshal(nil)
41 | }
42 |
43 | type hoverShadow Hover
44 | shadow := (*hoverShadow)(h)
45 | return json.Marshal(shadow)
46 | }
47 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/hover_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "encoding/json"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 |
11 | "github.com/caixw/apidoc/v7/core"
12 | )
13 |
14 | func TestHover_MarshalJSON(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | h := &Hover{}
18 | data, err := json.Marshal(h)
19 | a.NotError(err).Equal(string(data), `null`)
20 |
21 | h.Range = core.Range{
22 | Start: core.Position{Line: 0, Character: 10},
23 | End: core.Position{Line: 1, Character: 10},
24 | }
25 | data, err = json.Marshal(h)
26 | a.NotError(err).Equal(string(data), `null`)
27 |
28 | h.Contents = MarkupContent{
29 | Kind: MarkupKindPlainText,
30 | }
31 | data, err = json.MarshalIndent(h, "", "\t")
32 | a.NotError(err).Equal(string(data), `{
33 | "contents": {
34 | "kind": "plaintext",
35 | "value": ""
36 | },
37 | "range": {
38 | "start": {
39 | "line": 0,
40 | "character": 10
41 | },
42 | "end": {
43 | "line": 1,
44 | "character": 10
45 | }
46 | }
47 | }`)
48 | }
49 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/initialize_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | )
10 |
11 | func TestInitializeParams_Folders(t *testing.T) {
12 | a := assert.New(t, false)
13 |
14 | p := &InitializeParams{}
15 | a.Nil(p.Folders())
16 |
17 | p.RootPath = "../../lsp/protocol"
18 | a.Equal(p.Folders(), []WorkspaceFolder{
19 | {
20 | Name: "protocol",
21 | URI: "../../lsp/protocol",
22 | },
23 | })
24 |
25 | p.RootPath = "/../lsp/protocol2"
26 | a.Equal(p.Folders(), []WorkspaceFolder{
27 | {
28 | Name: "protocol2",
29 | URI: "/../lsp/protocol2",
30 | },
31 | })
32 |
33 | p.RootURI = "file:///lsp/protocol"
34 | a.Equal(p.Folders(), []WorkspaceFolder{
35 | {
36 | Name: "protocol",
37 | URI: "file:///lsp/protocol",
38 | },
39 | })
40 |
41 | p.WorkspaceFolders = []WorkspaceFolder{
42 | {
43 | Name: "f1",
44 | URI: "file:///f1",
45 | },
46 | {
47 | Name: "f2",
48 | URI: "https://example.com/f1",
49 | },
50 | }
51 | a.Equal(p.Folders(), []WorkspaceFolder{
52 | {
53 | Name: "f1",
54 | URI: "file:///f1",
55 | },
56 | {
57 | Name: "f2",
58 | URI: "https://example.com/f1",
59 | },
60 | })
61 | }
62 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/message.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | // SetTraceParams.Value 可用的值
6 | const (
7 | TraceValueOff = "off"
8 | TraceValueMessage = "message"
9 | TraceValueVerbose = "verbose"
10 | )
11 |
12 | // LogTraceParams $/logTrace 服务端下发参数
13 | type LogTraceParams struct {
14 | // The message to be logged.
15 | Message string `json:"message"`
16 |
17 | // Additional information that can be computed if the `trace` configuration is set to `'verbose'`
18 | Verbose string `json:"verbose,omitempty"`
19 | }
20 |
21 | // SetTraceParams $/setTrace 入口参数
22 | type SetTraceParams struct {
23 | // The new value that should be assigned to the trace setting.
24 | Value string `json:"value"`
25 | }
26 |
27 | // MessageType 传递的消息类型
28 | type MessageType int
29 |
30 | // MessageType 可能的值
31 | const (
32 | MessageTypeError MessageType = iota + 1 // An error message.
33 | MessageTypeWarning // A warning message.
34 | MessageTypeInfo // An information message.
35 | MessageTypeLog // A log message.
36 | )
37 |
38 | // LogMessageParams window/logMessage 传递的参数
39 | type LogMessageParams struct {
40 | // The message type. See {@link MessageType}
41 | Type MessageType `json:"type"`
42 |
43 | // The actual message
44 | Message string `json:"message"`
45 | }
46 |
47 | // IsValidTraceValue 是否是一个有效的 TraceValue
48 | func IsValidTraceValue(v string) bool {
49 | return v == TraceValueOff || v == TraceValueMessage || v == TraceValueVerbose
50 | }
51 |
52 | // BuildLogTrace 生成 logTrace 对象
53 | func BuildLogTrace(trace, message, verbose string) *LogTraceParams {
54 | switch trace {
55 | case TraceValueOff:
56 | return nil
57 | case TraceValueMessage:
58 | verbose = ""
59 | case TraceValueVerbose:
60 | default:
61 | panic("无效的 trace 值")
62 | }
63 |
64 | return &LogTraceParams{
65 | Message: message,
66 | Verbose: verbose,
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/message_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | )
10 |
11 | func TestIsValidTraceValue(t *testing.T) {
12 | a := assert.New(t, false)
13 | a.True(IsValidTraceValue(TraceValueMessage))
14 | a.False(IsValidTraceValue("invalid-value"))
15 | }
16 |
17 | func TestBuildLogTrace(t *testing.T) {
18 | a := assert.New(t, false)
19 |
20 | p := BuildLogTrace(TraceValueOff, "m1", "v2")
21 | a.Nil(p)
22 |
23 | p = BuildLogTrace(TraceValueMessage, "m1", "v2")
24 | a.NotNil(p).Equal(p.Message, "m1").Empty(p.Verbose)
25 |
26 | p = BuildLogTrace(TraceValueVerbose, "m1", "v2")
27 | a.NotNil(p).Equal(p.Message, "m1").Equal(p.Verbose, "v2")
28 |
29 | a.Panic(func() {
30 | BuildLogTrace("invalid-trace", "m2", "v2")
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/reference.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | // ReferenceClientCapabilities 客户端对 textDocument/references 的支持情况
6 | type ReferenceClientCapabilities struct {
7 | // Whether references supports dynamic registration.
8 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"`
9 | }
10 |
11 | // ReferenceParams textDocument/references 的请求参数
12 | type ReferenceParams struct {
13 | TextDocumentPositionParams
14 | WorkDoneProgressParams
15 | PartialResultParams
16 | Context struct {
17 | // Include the declaration of the current symbol.
18 | IncludeDeclaration bool `json:"includeDeclaration"`
19 | } `json:"context"`
20 | }
21 |
22 | // DefinitionClientCapabilities 客户端对 textDocument/definition 的支持情况
23 | type DefinitionClientCapabilities struct {
24 | // Whether definition supports dynamic registration.
25 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"`
26 |
27 | // The client supports additional metadata in the form of definition links.
28 | //
29 | // @since 3.14.0
30 | LinkSupport bool `json:"linkSupport,omitempty"`
31 | }
32 |
33 | // DefinitionParams textDocument/definition 服务的客户端请求参数
34 | type DefinitionParams struct {
35 | TextDocumentPositionParams
36 | WorkDoneProgressParams
37 | PartialResultParams
38 | }
39 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/server.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | // ServerCapabilities 服务端的兼容列表
6 | type ServerCapabilities struct {
7 | // Defines how text documents are synced.
8 | //
9 | // Is either a detailed structure defining each notification or
10 | // for backwards compatibility the TextDocumentSyncKind number.
11 | // If omitted it defaults to `TextDocumentSyncKind.None`.
12 | //
13 | // ServerCapabilitiesTextDocumentSyncOptions | TextDocumentSyncKind;
14 | TextDocumentSync *ServerCapabilitiesTextDocumentSyncOptions `json:"textDocumentSync"`
15 |
16 | // The server provides completion support.
17 | CompletionProvider *CompletionOptions `json:"completionProvider,omitempty"`
18 |
19 | // The server provides hover support.
20 | HoverProvider bool `json:"hoverProvider,omitempty"`
21 |
22 | // The server provides goto definition support.
23 | DefinitionProvider bool `json:"definitionProvider,omitempty"`
24 |
25 | // The server provides find references support.
26 | ReferencesProvider bool `json:"referencesProvider,omitempty"`
27 |
28 | // The server provides folding provider support.
29 | //
30 | // Since 3.10.0
31 | FoldingRangeProvider bool `json:"foldingRangeProvider,omitempty"`
32 |
33 | // The server provides folding provider support.
34 | //
35 | // Since 3.16.0
36 | //
37 | // SemanticTokensOptions | SemanticTokensRegistrationOptions
38 | SemanticTokensProvider any `json:"semanticTokensProvider,omitempty"`
39 |
40 | // The server provides workspace symbol support.
41 | WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider,omitempty"`
42 |
43 | // Workspace specific server capabilities
44 | Workspace *WorkspaceProvider `json:"workspace,omitempty"`
45 |
46 | // Experimental server capabilities.
47 | Experimental any `json:"experimental,omitempty"`
48 | }
49 |
50 | // SaveOptions Save options.
51 | type SaveOptions struct {
52 | // The client is supposed to include the content on save.
53 | IncludeText bool `json:"includeText,omitempty"`
54 | }
55 |
56 | // StaticRegistrationOptions static registration options to be returned in the initialize request.
57 | type StaticRegistrationOptions struct {
58 | // The id used to register the request. The id can be used to deregister
59 | // the request again. See also Registration#id.
60 | ID string `json:"id,omitempty"`
61 | }
62 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/textdocument_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | )
12 |
13 | func TestDidChangeTextDocumentParams_Blocks(t *testing.T) {
14 | a := assert.New(t, false)
15 |
16 | p := &DidChangeTextDocumentParams{}
17 | a.Empty(p.Blocks())
18 |
19 | p = &DidChangeTextDocumentParams{
20 | TextDocument: VersionedTextDocumentIdentifier{
21 | TextDocumentIdentifier: TextDocumentIdentifier{URI: core.FileURI("test.go")},
22 | },
23 | ContentChanges: []TextDocumentContentChangeEvent{
24 | {
25 | Range: &core.Range{End: core.Position{Line: 1, Character: 5}},
26 | Text: "text",
27 | },
28 | },
29 | }
30 | a.Equal(p.Blocks(), []core.Block{
31 | {
32 | Data: []byte("text"),
33 | Location: core.Location{
34 | URI: core.FileURI("test.go"),
35 | Range: core.Range{End: core.Position{Line: 1, Character: 5}},
36 | },
37 | },
38 | })
39 |
40 | // 未指定 Range
41 | p = &DidChangeTextDocumentParams{
42 | TextDocument: VersionedTextDocumentIdentifier{
43 | TextDocumentIdentifier: TextDocumentIdentifier{URI: core.FileURI("test.go")},
44 | },
45 | ContentChanges: []TextDocumentContentChangeEvent{
46 | {
47 | Text: "text",
48 | },
49 | },
50 | }
51 | a.Equal(p.Blocks(), []core.Block{
52 | {
53 | Data: []byte("text"),
54 | Location: core.Location{
55 | URI: core.FileURI("test.go"),
56 | },
57 | },
58 | })
59 |
60 | // 多个元素
61 | p = &DidChangeTextDocumentParams{
62 | TextDocument: VersionedTextDocumentIdentifier{
63 | TextDocumentIdentifier: TextDocumentIdentifier{URI: core.FileURI("test.go")},
64 | },
65 | ContentChanges: []TextDocumentContentChangeEvent{
66 | {
67 | Range: &core.Range{End: core.Position{Line: 1, Character: 5}},
68 | Text: "text",
69 | },
70 |
71 | {
72 | Range: &core.Range{End: core.Position{Line: 2, Character: 5}},
73 | Text: "text2",
74 | },
75 | },
76 | }
77 | a.Equal(p.Blocks(), []core.Block{
78 | {
79 | Data: []byte("text"),
80 | Location: core.Location{
81 | URI: core.FileURI("test.go"),
82 | Range: core.Range{End: core.Position{Line: 1, Character: 5}},
83 | },
84 | },
85 | {
86 | Data: []byte("text2"),
87 | Location: core.Location{
88 | URI: core.FileURI("test.go"),
89 | Range: core.Range{End: core.Position{Line: 2, Character: 5}},
90 | },
91 | },
92 | })
93 | }
94 |
--------------------------------------------------------------------------------
/internal/lsp/protocol/workspace_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package protocol
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/core"
11 | )
12 |
13 | func TestWorkspaceFolder_Contains(t *testing.T) {
14 | a := assert.New(t, false)
15 |
16 | data := []*struct {
17 | folder, path string
18 | contained bool
19 | }{
20 | {
21 | contained: true,
22 | },
23 | {
24 | folder: "file:///root/p1",
25 | path: "file:///root/p1/p2",
26 | contained: true,
27 | },
28 | {
29 | folder: "file:///root/p1",
30 | path: "/root/p1/p2",
31 | contained: true,
32 | },
33 | {
34 | folder: "http://root/p1",
35 | path: "http://root/p1/p2",
36 | contained: true,
37 | },
38 | {
39 | folder: "https://root/p1",
40 | path: "https://root/p1/p2",
41 | contained: true,
42 | },
43 | {
44 | folder: "http://root/p1",
45 | path: "https://root/p1/p2",
46 | },
47 | {
48 | folder: "http:///root/p1",
49 | path: "/root/p1/p2",
50 | },
51 | {
52 | folder: "file:///root/p1",
53 | path: "file:///root",
54 | },
55 | }
56 |
57 | for _, item := range data {
58 | folder := WorkspaceFolder{URI: core.URI(item.folder)}
59 | a.Equal(folder.Contains(core.URI(item.path)), item.contained)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/internal/lsp/reference.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lsp
4 |
5 | import (
6 | "reflect"
7 |
8 | "github.com/caixw/apidoc/v7/core"
9 | "github.com/caixw/apidoc/v7/internal/ast"
10 | "github.com/caixw/apidoc/v7/internal/lsp/protocol"
11 | )
12 |
13 | var (
14 | referencerType = reflect.TypeOf((*ast.Referencer)(nil)).Elem()
15 | definitionerType = reflect.TypeOf((*ast.Definitioner)(nil)).Elem()
16 | )
17 |
18 | // textDocument/references
19 | //
20 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
21 | func (s *server) textDocumentReferences(notify bool, in *protocol.ReferenceParams, out *[]core.Location) error {
22 | f := s.findFolder(in.TextDocument.URI)
23 | if f == nil {
24 | return nil
25 | }
26 |
27 | f.parsedMux.RLock()
28 | defer f.parsedMux.RUnlock()
29 |
30 | *out = references(f.doc, in.TextDocument.URI, in.Position, in.Context.IncludeDeclaration)
31 | return nil
32 | }
33 |
34 | // textDocument/definition
35 | //
36 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
37 | func (s *server) textDocumentDefinition(notify bool, in *protocol.DefinitionParams, out *[]core.Location) error {
38 | // NOTE: LSP 允许 out 的值是 null,而 jsonrpc 模块默认情况下是空值,而不是 nil,
39 | // 所以在可能的情况下,都尽量将其返回类型改为数组,
40 | // 或是像 protocol.Hover 一样为返回类型实现 json.Marshaler 接口。
41 | f := s.findFolder(in.TextDocument.URI)
42 | if f == nil {
43 | return nil
44 | }
45 |
46 | f.parsedMux.RLock()
47 | defer f.parsedMux.RUnlock()
48 |
49 | if r := f.doc.Search(in.TextDocument.URI, in.TextDocumentPositionParams.Position, definitionerType); r != nil {
50 | *out = []core.Location{r.(ast.Definitioner).Definition().Location}
51 | }
52 | return nil
53 | }
54 |
55 | func references(doc *ast.APIDoc, uri core.URI, pos core.Position, include bool) (locations []core.Location) {
56 | r := doc.Search(uri, pos, referencerType)
57 | if r == nil {
58 | return
59 | }
60 |
61 | referencer := r.(ast.Referencer)
62 | if include {
63 | locations = append(locations, referencer.Loc())
64 | }
65 |
66 | for _, ref := range referencer.References() {
67 | locations = append(locations, ref.Location)
68 | }
69 |
70 | return
71 | }
72 |
--------------------------------------------------------------------------------
/internal/lsp/server_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lsp
4 |
5 | import (
6 | "io/ioutil"
7 | "log"
8 | "os"
9 | "testing"
10 |
11 | "github.com/issue9/assert/v3"
12 | "github.com/issue9/jsonrpc"
13 |
14 | "github.com/caixw/apidoc/v7/internal/lsp/protocol"
15 | )
16 |
17 | func newTestServer(header bool, info, erro *log.Logger) *server {
18 | return newServe(jsonrpc.NewStreamTransport(header, os.Stdin, os.Stdout, nil), info, erro)
19 | }
20 |
21 | func TestServer_setTrace(t *testing.T) {
22 | a := assert.New(t, false)
23 | s := newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
24 |
25 | err := s.setTrace(false, &protocol.SetTraceParams{}, nil)
26 | a.Error(err)
27 | jerr, ok := err.(*jsonrpc.Error)
28 | a.True(ok).Equal(jerr.Code, jsonrpc.CodeInvalidParams)
29 | a.Equal(s.trace, protocol.TraceValueOff)
30 |
31 | err = s.setTrace(false, &protocol.SetTraceParams{Value: protocol.TraceValueOff}, nil)
32 | a.NotError(err).Equal(s.trace, protocol.TraceValueOff)
33 |
34 | err = s.setTrace(false, &protocol.SetTraceParams{Value: protocol.TraceValueVerbose}, nil)
35 | a.NotError(err).Equal(s.trace, protocol.TraceValueVerbose)
36 | }
37 |
--------------------------------------------------------------------------------
/internal/lsp/workspace.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lsp
4 |
5 | import (
6 | "github.com/issue9/sliceutil"
7 |
8 | "github.com/caixw/apidoc/v7/internal/locale"
9 | "github.com/caixw/apidoc/v7/internal/lsp/protocol"
10 | )
11 |
12 | // workspace/workspaceFolders
13 | //
14 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders
15 | func (s *server) workspaceWorkspaceFolders() error {
16 | return s.Send("workspace/workspaceFolders", nil, func(folders *[]protocol.WorkspaceFolder) error {
17 | s.workspaceMux.Lock()
18 | defer s.workspaceMux.Unlock()
19 |
20 | for _, f := range s.folders {
21 | f.close()
22 | }
23 | s.folders = s.folders[:0]
24 |
25 | s.appendFolders(*folders...)
26 | return nil
27 | })
28 | }
29 |
30 | // workspace/didChangeWorkspaceFolders
31 | //
32 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeWorkspaceFolders
33 | func (s *server) workspaceDidChangeWorkspaceFolders(notify bool, in *protocol.DidChangeWorkspaceFoldersParams, out *any) error {
34 | if s.getState() != serverInitialized {
35 | return newError(ErrInvalidRequest, locale.ErrInvalidLSPState)
36 | }
37 |
38 | s.workspaceMux.Lock()
39 | defer s.workspaceMux.Unlock()
40 |
41 | old := s.folders
42 | s.folders = sliceutil.Delete(s.folders, func(i *folder) bool {
43 | return sliceutil.Count(in.Event.Removed, func(j protocol.WorkspaceFolder) bool {
44 | return j.URI == i.URI
45 | }) > 0
46 | })
47 |
48 | deleted := old[len(s.folders):]
49 | for _, f := range deleted {
50 | f.close()
51 | }
52 |
53 | s.appendFolders(in.Event.Added...)
54 |
55 | return nil
56 | }
57 |
--------------------------------------------------------------------------------
/internal/lsp/workspace_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package lsp
4 |
5 | import (
6 | "io/ioutil"
7 | "log"
8 | "testing"
9 |
10 | "github.com/issue9/assert/v3"
11 | "github.com/issue9/jsonrpc"
12 |
13 | "github.com/caixw/apidoc/v7/core/messagetest"
14 | "github.com/caixw/apidoc/v7/internal/lsp/protocol"
15 | )
16 |
17 | func TestServer_workspaceDidChangeWorkspaceFolders(t *testing.T) {
18 | a := assert.New(t, false)
19 |
20 | s := newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
21 | in := &protocol.DidChangeWorkspaceFoldersParams{}
22 | err := s.workspaceDidChangeWorkspaceFolders(false, in, nil)
23 | a.Error(err)
24 | jerr, ok := err.(*jsonrpc.Error)
25 | a.True(ok).Equal(jerr.Code, ErrInvalidRequest)
26 |
27 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
28 | s.setState(serverInitialized)
29 | in = &protocol.DidChangeWorkspaceFoldersParams{}
30 | a.NotError(s.workspaceDidChangeWorkspaceFolders(false, in, nil))
31 | a.Equal(0, len(s.folders))
32 |
33 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0))
34 | s.setState(serverInitialized)
35 | s.folders = []*folder{
36 | {
37 | srv: s,
38 | WorkspaceFolder: protocol.WorkspaceFolder{Name: "n0", URI: "file:///n0"},
39 | h: messagetest.NewMessageHandler().Handler,
40 | },
41 | {
42 | srv: s,
43 | WorkspaceFolder: protocol.WorkspaceFolder{Name: "n1", URI: "file:///n1"},
44 | h: messagetest.NewMessageHandler().Handler,
45 | },
46 | }
47 |
48 | in = &protocol.DidChangeWorkspaceFoldersParams{
49 | Event: protocol.WorkspaceFoldersChangeEvent{
50 | Added: []protocol.WorkspaceFolder{
51 | {Name: "n3", URI: "file:///n3"},
52 | },
53 | Removed: []protocol.WorkspaceFolder{
54 | {Name: "n1", URI: "file:///n1"},
55 | },
56 | },
57 | }
58 | a.NotError(s.workspaceDidChangeWorkspaceFolders(false, in, nil))
59 | a.Equal(2, len(s.folders))
60 | }
61 |
--------------------------------------------------------------------------------
/internal/mock/options.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package mock
4 |
5 | import (
6 | "strconv"
7 |
8 | "github.com/caixw/apidoc/v7/internal/ast"
9 | )
10 |
11 | // GenOptions 生成随机数据的函数
12 | type GenOptions struct {
13 | // 返回一个随机的数值
14 | //
15 | // 可以是浮点和整数类型。
16 | Number func(p *ast.Param) any
17 |
18 | // 返回一个随机长度的字符串
19 | String func(p *ast.Param) string
20 |
21 | // 返回一个随机的布尔值
22 | Bool func() bool
23 |
24 | // 返回一个随机的数值
25 | //
26 | // 该数值被用于声明 slice 长度,所以必须为正整数。
27 | SliceSize func() int
28 |
29 | // 返回一个介于 [0, max] 之间的数值
30 | //
31 | // 该数值被用于从数组中获取其中的某个元素。
32 | Index func(max int) int
33 | }
34 |
35 | func isEnum(p *ast.Param) bool {
36 | return len(p.Enums) > 0
37 | }
38 |
39 | func (g *GenOptions) generateBool() bool {
40 | return g.Bool()
41 | }
42 |
43 | func (g *GenOptions) generateNumber(p *ast.Param) any {
44 | if isEnum(p) {
45 | index := g.Index(len(p.Enums))
46 | v, err := strconv.ParseInt(p.Enums[index].Value.V(), 10, 32)
47 | if err != nil { // 这属于文档定义错误,直接 panic
48 | panic(err)
49 | }
50 | return v
51 | }
52 | return g.Number(p)
53 | }
54 |
55 | func (g *GenOptions) generateString(p *ast.Param) string {
56 | if isEnum(p) {
57 | return p.Enums[g.Index(len(p.Enums))].Value.V()
58 | }
59 | return g.String(p)
60 | }
61 |
62 | func (g *GenOptions) generateSliceSize() int {
63 | return g.SliceSize()
64 | }
65 |
--------------------------------------------------------------------------------
/internal/mock/options_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package mock
4 |
5 | import "github.com/caixw/apidoc/v7/internal/ast"
6 |
7 | const indent = " "
8 |
9 | var testOptions = &GenOptions{
10 | Number: func(p *ast.Param) any { return 1024 },
11 | String: func(p *ast.Param) string {
12 | switch p.Type.V() {
13 | case ast.TypeEmail:
14 | return "user@example.com"
15 | case ast.TypeURL:
16 | return "https://example.com"
17 | case ast.TypeDate:
18 | return "2020-01-02"
19 | case ast.TypeTime:
20 | return "15:16:17Z"
21 | case ast.TypeDateTime:
22 | return "2020-01-02T15:16:17Z"
23 | }
24 | return "1024"
25 | },
26 | Bool: func() bool { return true },
27 | SliceSize: func() int { return 5 },
28 | Index: func(max int) int { return 0 },
29 | }
30 |
--------------------------------------------------------------------------------
/internal/node/value.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package node
4 |
5 | import (
6 | "fmt"
7 | "reflect"
8 | "unicode"
9 | )
10 |
11 | // Value 表示 XML 节点的值的反射表示方式
12 | type Value struct {
13 | reflect.Value
14 | Omitempty bool
15 | Name string // 节点的名称
16 |
17 | // 当前值可能未初始化,所以保存 usage 的值,
18 | // 等 Value 初始化之后再赋值给 Base.UsageKey
19 | Usage string
20 | }
21 |
22 | // NewValue 声明 *Value 实例
23 | func NewValue(name string, v reflect.Value, omitempty bool, usage string) *Value {
24 | return &Value{
25 | Name: name,
26 | Value: v,
27 | Omitempty: omitempty,
28 | Usage: usage,
29 | }
30 | }
31 |
32 | // ParseValue 分析 v 并返回 *Value 实例
33 | //
34 | // 与 NewValue 的不同在于,ParseValue 会分析对象字段中是否带有 meta 的结构体标签,
35 | // 如果有才初始化 *Value 对象,否则返回 nil。
36 | func ParseValue(v reflect.Value) *Value {
37 | v = RealValue(v)
38 | t := v.Type()
39 |
40 | if t.Kind() != reflect.Struct {
41 | panic(fmt.Sprintf("%s 的 Kind() 必须为 reflect.Struct", v.Type()))
42 | }
43 |
44 | num := t.NumField()
45 | for i := 0; i < num; i++ {
46 | field := t.Field(i)
47 | if field.Anonymous || unicode.IsLower(rune(field.Name[0])) || field.Tag.Get(TagName) == "-" {
48 | continue
49 | }
50 |
51 | if name, node, usage, omitempty := parseTag(field); node == meta {
52 | return NewValue(name, v, omitempty, usage)
53 | }
54 | }
55 | return nil
56 | }
57 |
58 | // IsPrimitive 是否为有效的 Go 原始类型
59 | func IsPrimitive(v reflect.Value) bool {
60 | return v.IsValid() &&
61 | (v.Kind() == reflect.String || (v.Kind() >= reflect.Bool && v.Kind() <= reflect.Complex128))
62 | }
63 |
64 | // RealType 获取指针指向的类型
65 | func RealType(t reflect.Type) reflect.Type {
66 | for t.Kind() == reflect.Ptr {
67 | t = t.Elem()
68 | }
69 | return t
70 | }
71 |
72 | // RealValue 获取指针指向的值
73 | //
74 | // 如果未初始化,则会对其进行初始化。
75 | func RealValue(v reflect.Value) reflect.Value {
76 | for v.Kind() == reflect.Ptr {
77 | if v.IsNil() {
78 | v.Set(reflect.New(v.Type().Elem()))
79 | }
80 | v = v.Elem()
81 | }
82 | return v
83 | }
84 |
--------------------------------------------------------------------------------
/internal/node/value_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package node
4 |
5 | import (
6 | "reflect"
7 | "testing"
8 |
9 | "github.com/issue9/assert/v3"
10 | )
11 |
12 | func TestParseValue(t *testing.T) {
13 | a := assert.New(t, false)
14 |
15 | v := ParseValue(reflect.ValueOf(intTag{}))
16 | a.Equal(v.Name, "number").
17 | Equal(v.Usage, "usage-number").
18 | False(v.Omitempty)
19 |
20 | v = ParseValue(reflect.ValueOf(struct{}{}))
21 | a.Nil(v)
22 |
23 | v = ParseValue(reflect.ValueOf(&struct {
24 | Value int `apidoc:"-"`
25 | }{}))
26 | a.Nil(v)
27 |
28 | a.Panic(func() {
29 | v = ParseValue(reflect.ValueOf(1))
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/internal/openapi/errors.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import "github.com/caixw/apidoc/v7/core"
6 |
7 | // 数据验证接口
8 | type sanitizer interface {
9 | Sanitize() *core.Error
10 | }
11 |
--------------------------------------------------------------------------------
/internal/openapi/info.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "github.com/issue9/validation/is"
7 | "github.com/issue9/version"
8 |
9 | "github.com/caixw/apidoc/v7/core"
10 | "github.com/caixw/apidoc/v7/internal/ast"
11 | "github.com/caixw/apidoc/v7/internal/locale"
12 | )
13 |
14 | // Info 接口文档的基本信息
15 | type Info struct {
16 | Title string `json:"title" yaml:"title"`
17 | Description string `json:"description,omitempty" yaml:"description,omitempty"`
18 | TermsOfService string `json:"termsOfService,omitempty" json:"termsOfService,omitempty"`
19 | Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"`
20 | License *License `json:"license,omitempty" yaml:"license,omitempty"`
21 | Version string `json:"version" yaml:"version"`
22 | }
23 |
24 | // Contact 描述联系方式
25 | type Contact struct {
26 | Name string `json:"name,omitempty" yaml:"name,omitempty"`
27 | URL string `json:"url,omitempty" yaml:"url,omitempty"`
28 | Email string `json:"email,omitempty" yaml:"email,omitempty"`
29 | }
30 |
31 | // License 授权信息
32 | type License struct {
33 | Name string `json:"name" yaml:"name"`
34 | URL string `json:"url,omitempty" yaml:"url,omitempty"`
35 | }
36 |
37 | func (info *Info) sanitize() *core.Error {
38 | if info.Title == "" {
39 | return core.NewError(locale.ErrIsEmpty, "title").WithField("title")
40 | }
41 |
42 | if !version.SemVerValid(info.Version) {
43 | return core.NewError(locale.ErrInvalidFormat).WithField("version")
44 | }
45 |
46 | if info.TermsOfService != "" && !is.URL(info.TermsOfService) {
47 | return core.NewError(locale.ErrInvalidFormat).WithField("termsOfService")
48 | }
49 |
50 | if info.Contact != nil {
51 | if err := info.Contact.sanitize(); err != nil {
52 | err.Field = "contact." + err.Field
53 | return err
54 | }
55 | }
56 |
57 | if info.License != nil {
58 | if err := info.License.sanitize(); err != nil {
59 | err.Field = "license." + err.Field
60 | return err
61 | }
62 | }
63 |
64 | return nil
65 | }
66 |
67 | func (l *License) sanitize() *core.Error {
68 | if l.URL != "" && !is.URL(l.URL) {
69 | return core.NewError(locale.ErrInvalidFormat).WithField("url")
70 | }
71 |
72 | return nil
73 | }
74 |
75 | func newLicense(l *ast.Link) *License {
76 | if l == nil {
77 | return nil
78 | }
79 |
80 | return &License{
81 | Name: l.Text.V(),
82 | URL: l.URL.V(),
83 | }
84 | }
85 |
86 | func newContact(c *ast.Contact) *Contact {
87 | if c == nil {
88 | return nil
89 | }
90 |
91 | return &Contact{
92 | Name: c.Name.V(),
93 | URL: c.URL.V(),
94 | Email: c.Email.V(),
95 | }
96 | }
97 |
98 | func (c *Contact) sanitize() *core.Error {
99 | if c.URL != "" && !is.URL(c.URL) {
100 | return core.NewError(locale.ErrInvalidFormat).WithField("url")
101 | }
102 |
103 | if c.Email != "" && !is.Email(c.Email) {
104 | return core.NewError(locale.ErrInvalidFormat).WithField("email")
105 | }
106 |
107 | return nil
108 | }
109 |
--------------------------------------------------------------------------------
/internal/openapi/info_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/internal/ast"
11 | "github.com/caixw/apidoc/v7/internal/xmlenc"
12 | )
13 |
14 | func TestNewContact(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | input := &ast.Contact{
18 | Email: &ast.Element{Content: ast.Content{Value: "user@example.com"}},
19 | URL: &ast.Element{Content: ast.Content{Value: "https://example.com"}},
20 | Name: &ast.Attribute{Value: xmlenc.String{Value: "name"}},
21 | }
22 |
23 | output := newContact(input)
24 | a.Equal(output.Email, input.Email.Content.Value)
25 |
26 | output = newContact(nil)
27 | a.Nil(output)
28 | }
29 |
30 | func TestInfo_sanitize(t *testing.T) {
31 | a := assert.New(t, false)
32 |
33 | info := &Info{}
34 | a.Error(info.sanitize())
35 |
36 | info.Title = "title"
37 | a.Error(info.sanitize())
38 |
39 | info.Title = "title"
40 | info.Version = "3.3.1"
41 | a.NotError(info.sanitize())
42 |
43 | info.TermsOfService = "invalid url"
44 | a.Error(info.sanitize())
45 |
46 | info.TermsOfService = "https://example.com"
47 | a.NotError(info.sanitize())
48 |
49 | // contact
50 |
51 | info.Contact = &Contact{
52 | Name: "name",
53 | Email: "invalid-email",
54 | URL: "invalid-url",
55 | }
56 | a.Error(info.sanitize())
57 |
58 | info.Contact.URL = "https://example.com"
59 | a.Error(info.sanitize())
60 |
61 | info.Contact.Email = "user@example.com"
62 | a.NotError(info.sanitize())
63 |
64 | // License
65 | info.License = &License{
66 | Name: "license",
67 | URL: "invalid-url",
68 | }
69 | a.Error(info.sanitize())
70 |
71 | info.License.URL = "https://example.com"
72 | a.NotError(info.sanitize())
73 | }
74 |
--------------------------------------------------------------------------------
/internal/openapi/openapi_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | "github.com/issue9/version"
10 | )
11 |
12 | func TestLatestVersion(t *testing.T) {
13 | a := assert.New(t, false)
14 |
15 | a.True(version.SemVerValid(LatestVersion))
16 | }
17 |
18 | func TestOpenAPI_sanitize(t *testing.T) {
19 | a := assert.New(t, false)
20 |
21 | oa := &OpenAPI{Info: &Info{
22 | Title: "title",
23 | Version: "3.3.3",
24 | },
25 | Paths: map[string]*PathItem{
26 | "/api": {
27 | Get: &Operation{
28 | Responses: map[string]*Response{
29 | "json": {
30 | Description: "desc",
31 | },
32 | },
33 | },
34 | },
35 | },
36 | }
37 | a.NotError(oa.sanitize())
38 | a.Equal(oa.OpenAPI, LatestVersion)
39 | }
40 |
41 | func TestExternalDocumentation_sanitize(t *testing.T) {
42 | a := assert.New(t, false)
43 |
44 | ed := &ExternalDocumentation{}
45 | a.Error(ed.sanitize())
46 |
47 | ed.URL = "url"
48 | a.Error(ed.sanitize())
49 |
50 | ed.URL = "https://example.com"
51 | a.NotError(ed.sanitize())
52 | }
53 |
54 | func TestTag_sanitize(t *testing.T) {
55 | a := assert.New(t, false)
56 |
57 | tag := &Tag{
58 | ExternalDocs: &ExternalDocumentation{},
59 | }
60 | a.Error(tag.sanitize())
61 |
62 | tag.Name = "name"
63 | a.Error(tag.sanitize())
64 |
65 | tag.ExternalDocs.URL = "https://example.com"
66 | a.NotError(tag.sanitize())
67 | }
68 |
--------------------------------------------------------------------------------
/internal/openapi/parameter.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "github.com/caixw/apidoc/v7/core"
7 | "github.com/caixw/apidoc/v7/internal/locale"
8 | )
9 |
10 | // Parameter.IN 的可选值
11 | const (
12 | ParameterINPath = "path"
13 | ParameterINQuery = "query"
14 | ParameterINHeader = "header"
15 | ParameterINCookie = "cookie"
16 | )
17 |
18 | // Header 即 Parameter 的别名,但 Name 字段必须不能存在。
19 | type Header Parameter
20 |
21 | // Parameter 参数信息
22 | // 可同时作用于路径参数、请求参数、报头内容和 Cookie 值。
23 | type Parameter struct {
24 | Style
25 | Name string `json:"name,omitempty" yaml:"name,omitempty"`
26 | IN string `json:"in,omitempty" yaml:"in,omitempty"`
27 | Description string `json:"description,omitempty" yaml:"description,omitempty"`
28 | Required bool `json:"required,omitempty" yaml:"required,omitempty"`
29 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
30 | AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
31 | Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"`
32 | Example ExampleValue `json:"example,omitempty" yaml:"example,omitempty"`
33 | Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"`
34 | Content map[string]*MediaType `json:"content,omitempty" yaml:"content,omitempty"`
35 |
36 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
37 | }
38 |
39 | func (p *Parameter) sanitize() *core.Error {
40 | if err := p.Style.sanitize(); err != nil {
41 | return err
42 | }
43 |
44 | switch p.IN {
45 | case ParameterINCookie, ParameterINHeader, ParameterINPath, ParameterINQuery:
46 | default:
47 | return core.NewError(locale.ErrInvalidValue).WithField("in")
48 | }
49 |
50 | return nil
51 | }
52 |
53 | func (h *Header) sanitize() *core.Error {
54 | if err := h.Style.sanitize(); err != nil {
55 | return err
56 | }
57 |
58 | if h.IN != "" {
59 | return core.NewError(locale.ErrInvalidValue).WithField("in")
60 | }
61 |
62 | if h.Name != "" {
63 | return core.NewError(locale.ErrInvalidValue).WithField("name")
64 | }
65 |
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/internal/openapi/parameter_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | )
10 |
11 | func TestParameter_sanitize(t *testing.T) {
12 | a := assert.New(t, false)
13 |
14 | p := &Parameter{}
15 | a.Error(p.sanitize())
16 |
17 | p.Style = Style{Style: StyleDeepObject}
18 | a.Error(p.sanitize())
19 |
20 | p.IN = ParameterINPath
21 | a.NotError(p.sanitize())
22 | }
23 |
24 | func TestHeader_sanitize(t *testing.T) {
25 | a := assert.New(t, false)
26 |
27 | p := &Header{}
28 | a.Error(p.sanitize())
29 |
30 | p.Style = Style{Style: StyleDeepObject}
31 | a.NotError(p.sanitize())
32 |
33 | // IN 只能为空
34 | p.IN = ParameterINPath
35 | a.Error(p.sanitize())
36 |
37 | p.IN = ""
38 | p.Name = "test"
39 | a.Error(p.sanitize())
40 |
41 | p.Name = ""
42 | a.NotError(p.sanitize())
43 | }
44 |
--------------------------------------------------------------------------------
/internal/openapi/parse_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "encoding/json"
7 | "net/http"
8 | "strconv"
9 | "testing"
10 |
11 | "github.com/issue9/assert/v3"
12 |
13 | "github.com/caixw/apidoc/v7/core"
14 | "github.com/caixw/apidoc/v7/internal/ast/asttest"
15 | )
16 |
17 | func TestJSON(t *testing.T) {
18 | a := assert.New(t, false)
19 | data, err := JSON(asttest.Get())
20 | a.NotError(err).NotNil(data)
21 |
22 | openapi := &OpenAPI{}
23 | a.NotError(json.Unmarshal(data, openapi)).
24 | Equal(3, len(openapi.Tags)).
25 | Equal(1, len(openapi.Paths)).
26 | Equal(openapi.ExternalDocs.URL, core.OfficialURL).
27 | NotEmpty(openapi.ExternalDocs.Description)
28 |
29 | path := openapi.Paths["/users"]
30 | a.NotNil(path)
31 | a.NotNil(path.Post).NotNil(path.Get).Nil(path.Patch)
32 | a.True(path.Post.Deprecated)
33 | a.Equal(path.Post.Summary, "summary")
34 | a.NotNil(path.Get).NotNil(path.Post)
35 |
36 | get := path.Get
37 | a.Equal(1, len(get.Responses))
38 | a.Equal(len(get.Responses[strconv.Itoa(http.StatusOK)].Headers), 1)
39 | }
40 |
41 | func TestYAML(t *testing.T) {
42 | a := assert.New(t, false)
43 | data, err := YAML(asttest.Get())
44 | a.NotError(err).NotNil(data)
45 | }
46 |
--------------------------------------------------------------------------------
/internal/openapi/schema_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/internal/ast"
11 | "github.com/caixw/apidoc/v7/internal/xmlenc"
12 | )
13 |
14 | func TestNewSchema(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | d := &ast.APIDoc{}
18 | input := &ast.Param{
19 | Name: &ast.Attribute{Value: xmlenc.String{Value: "name"}},
20 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeBool}},
21 | Deprecated: &ast.VersionAttribute{Value: xmlenc.String{Value: "v1.1.0"}},
22 | Default: &ast.Attribute{Value: xmlenc.String{Value: "true"}},
23 | Optional: &ast.BoolAttribute{Value: ast.Bool{Value: true}},
24 | Array: &ast.BoolAttribute{Value: ast.Bool{Value: false}},
25 | Summary: &ast.Attribute{Value: xmlenc.String{Value: "summary"}},
26 | }
27 | output := newSchema(d, input, true)
28 | a.Equal(output.Type, TypeBool).
29 | True(output.Deprecated).
30 | Equal(output.Title, input.Summary.V())
31 |
32 | input.Array = &ast.BoolAttribute{Value: ast.Bool{Value: true}}
33 | output = newSchema(d, input, true)
34 | a.Equal(output.Type, TypeArray).
35 | Equal(output.Items.Type, TypeBool).
36 | True(output.Items.Deprecated).
37 | Equal(output.Items.Title, input.Summary.V())
38 |
39 | input.Enums = []*ast.Enum{
40 | {
41 | Value: &ast.Attribute{Value: xmlenc.String{Value: "v1"}},
42 | Summary: &ast.Attribute{Value: xmlenc.String{Value: "s1"}},
43 | },
44 | {
45 | Value: &ast.Attribute{Value: xmlenc.String{Value: "v2"}},
46 | Description: &ast.Richtext{
47 | Text: &ast.CData{Value: xmlenc.String{Value: "s2"}},
48 | },
49 | Deprecated: &ast.VersionAttribute{Value: xmlenc.String{Value: "1.0.1"}},
50 | },
51 | }
52 | output = newSchema(d, input, false)
53 | a.Equal(output.Type, TypeBool).
54 | Equal(2, len(output.Enum)).
55 | Equal(output.Enum, []string{"v1", "v2"})
56 |
57 | input = &ast.Param{
58 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeNumber}},
59 | Items: []*ast.Param{
60 | {
61 | Name: &ast.Attribute{Value: xmlenc.String{Value: "p1"}},
62 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeString}},
63 | },
64 | {
65 | Name: &ast.Attribute{Value: xmlenc.String{Value: "p2"}},
66 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeNumber}},
67 | },
68 | },
69 | }
70 | output = newSchema(d, input, true)
71 | a.Equal(output.Type, "").
72 | Equal(len(input.Items), len(output.Properties)).
73 | Equal(output.Properties["p1"].Type, TypeString).
74 | Equal(output.Properties["p2"].Type, TypeDouble)
75 |
76 | a.NotError(output.sanitize())
77 | }
78 |
--------------------------------------------------------------------------------
/internal/openapi/security.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | // SecurityScheme.IN 的可选值
6 | const (
7 | SecurityInQuery = "query"
8 | SecurityInHeader = "header"
9 | SecurityInCookie = "cookie"
10 | )
11 |
12 | // Security.Type 的可选值
13 | const (
14 | SecurityTypeAPIKey = "apikey"
15 | SecurityTypeHTTP = "http"
16 | SecurityTypeOAuth2 = "oauth2"
17 | SecurityTypeOpenIDConnect = "openIdConnect"
18 | )
19 |
20 | // SecurityRequirement Object
21 | //
22 | // 键名指向的是 Components.SecuritySchemes 中的名称。
23 | // 若 SecurityScheme.Type 是 oauth2 或是 openIDConnect,
24 | // 则 SecurityRequirement 的键值必须是个空值,否则键值为一个 scope 列表。
25 | type SecurityRequirement map[string][]string
26 |
27 | // SecurityScheme Object
28 | type SecurityScheme struct {
29 | Type string `json:"type" yaml:"type"`
30 | Description string `json:"description,omitempty" yaml:"description,omitempty"`
31 | Name string `json:"name" yaml:"name"` // 报头或是 cookie 的名称
32 | IN string `json:"in" yaml:"in"` // 位置, header, query 和 cookie
33 | Scheme string `json:"scheme" yaml:"scheme"`
34 | BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"`
35 | Flows *OAuthFlows `json:"flows" yaml:"flows"`
36 | OpenIDConnectURL string `json:"openIdConnectUrl" yaml:"openIdConnectUrl"`
37 |
38 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
39 | }
40 |
41 | // OAuthFlows Object
42 | type OAuthFlows struct {
43 | Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"`
44 | Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"`
45 | ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"`
46 | AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"`
47 | }
48 |
49 | // OAuthFlow Object
50 | type OAuthFlow struct {
51 | AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
52 | TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
53 | RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
54 | Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"`
55 | }
56 |
--------------------------------------------------------------------------------
/internal/openapi/server.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "strings"
7 |
8 | "github.com/caixw/apidoc/v7/core"
9 | "github.com/caixw/apidoc/v7/internal/ast"
10 | "github.com/caixw/apidoc/v7/internal/locale"
11 | )
12 |
13 | // 去掉 URL 中的 {} 模板参数。使其符合 is.URL 的判断规则
14 | var urlreplace = strings.NewReplacer("{", "", "}", "")
15 |
16 | // Server 服务器描述信息
17 | type Server struct {
18 | URL string `json:"url" yaml:"url"`
19 | Description string `json:"description,omitempty" yaml:"description,omitempty"`
20 | Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"`
21 | }
22 |
23 | // ServerVariable Server 中 URL 模板中对应的参数变量值
24 | type ServerVariable struct {
25 | Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"`
26 | Default string `json:"default" yaml:"default"`
27 | Description string `json:"description,omitempty" yaml:"description,omitempty"`
28 | }
29 |
30 | func newServer(srv *ast.Server) *Server {
31 | desc := srv.Summary.V()
32 | if srv.Description != nil && srv.Description.Text != nil {
33 | desc = srv.Description.V()
34 | }
35 |
36 | return &Server{
37 | URL: srv.URL.V(),
38 | Description: desc,
39 | }
40 | }
41 |
42 | func (srv *Server) sanitize() *core.Error {
43 | url := urlreplace.Replace(srv.URL)
44 | if url == "" { // 可以是 / 未必是一个 URL
45 | return core.NewError(locale.ErrIsEmpty, "url").WithField("url")
46 | }
47 |
48 | for key, val := range srv.Variables {
49 | if err := val.sanitize(); err != nil {
50 | err.Field = "variables[" + key + "]." + err.Field
51 | return err
52 | }
53 |
54 | k := "{" + key + "}"
55 | if !strings.Contains(srv.URL, k) {
56 | return core.NewError(locale.ErrInvalidValue).WithField("variables[" + key + "]")
57 | }
58 | }
59 |
60 | return nil
61 | }
62 |
63 | func (v *ServerVariable) sanitize() *core.Error {
64 | if v.Default == "" {
65 | return core.NewError(locale.ErrIsEmpty, "default").WithField("default")
66 | }
67 |
68 | if len(v.Enum) == 0 {
69 | return nil
70 | }
71 |
72 | found := false
73 | for _, item := range v.Enum {
74 | if item == v.Default {
75 | found = true
76 | break
77 | }
78 | }
79 |
80 | if !found {
81 | return core.NewError(locale.ErrInvalidValue).WithField("default")
82 | }
83 |
84 | return nil
85 | }
86 |
--------------------------------------------------------------------------------
/internal/openapi/server_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/internal/ast"
11 | "github.com/caixw/apidoc/v7/internal/xmlenc"
12 | )
13 |
14 | func TestNewServer(t *testing.T) {
15 | a := assert.New(t, false)
16 |
17 | input := &ast.Server{
18 | URL: &ast.Attribute{Value: xmlenc.String{Value: "https://example.com"}},
19 | Name: &ast.Attribute{Value: xmlenc.String{Value: "name"}},
20 | Summary: &ast.Attribute{Value: xmlenc.String{Value: "summary"}},
21 | }
22 |
23 | output := newServer(input)
24 | a.NotNil(output).
25 | Equal(output.URL, "https://example.com").
26 | Equal(output.Description, "summary")
27 |
28 | input.Description = &ast.Richtext{Text: &ast.CData{Value: xmlenc.String{Value: "desc"}}}
29 | output = newServer(input)
30 | a.NotNil(output).
31 | Equal(output.URL, "https://example.com").
32 | Equal(output.Description, "desc")
33 | }
34 |
35 | func TestServer_sanitize(t *testing.T) {
36 | a := assert.New(t, false)
37 |
38 | srv := &Server{}
39 | a.Error(srv.sanitize())
40 |
41 | srv.URL = "https://example.com/{tpl1}/{tpl2}/path3"
42 | a.NotError(srv.sanitize())
43 |
44 | srv.Variables = map[string]*ServerVariable{
45 | "tpl1": {Default: "1"},
46 | "tpl2": {Default: "2", Enum: []string{"1", "2"}},
47 | }
48 | a.NotError(srv.sanitize())
49 |
50 | // variable 不在 URL 中
51 | srv.Variables = map[string]*ServerVariable{
52 | "tpl3": {Default: "1"},
53 | }
54 | a.Error(srv.sanitize())
55 |
56 | // variables 存在错误
57 | srv.Variables = map[string]*ServerVariable{
58 | "tpl2": {Default: "not-exists", Enum: []string{"1", "2"}},
59 | }
60 | a.Error(srv.sanitize())
61 | }
62 |
63 | func TestServerVariable_sanitize(t *testing.T) {
64 | a := assert.New(t, false)
65 |
66 | sv := &ServerVariable{}
67 | a.Error(sv.sanitize())
68 |
69 | sv.Enum = []string{"e1", "e2"}
70 | a.Error(sv.sanitize())
71 |
72 | sv.Default = "not-in-enum"
73 | a.Error(sv.sanitize())
74 |
75 | sv.Default = "e1"
76 | a.NotError(sv.sanitize())
77 | }
78 |
--------------------------------------------------------------------------------
/internal/openapi/style.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "github.com/caixw/apidoc/v7/core"
7 | "github.com/caixw/apidoc/v7/internal/locale"
8 | )
9 |
10 | // Style.Style 的可选值
11 | const (
12 | StyleMatrix = "matrix"
13 | StyleLabel = "label"
14 | StyleForm = "form"
15 | StyleSimple = "simple"
16 | StyleSpaceDelimited = "spaceDelimited"
17 | StylePipeDelimited = "pipeDelimited"
18 | StyleDeepObject = "deepObject"
19 | )
20 |
21 | // Style 民法风格的相关定义
22 | //
23 | // 不直接作用于对象,被部分对象包含,比如 Encoding 和 Parameter 等
24 | type Style struct {
25 | Style string `json:"style,omitempty" yaml:"style,omitempty"`
26 | Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"`
27 | AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"`
28 | }
29 |
30 | func (style *Style) sanitize() *core.Error {
31 | switch style.Style {
32 | case StyleMatrix, StyleLabel, StyleForm, StyleSimple, StyleSpaceDelimited, StylePipeDelimited, StyleDeepObject:
33 | default:
34 | return core.NewError(locale.ErrInvalidValue).WithField("style")
35 | }
36 |
37 | return nil
38 | }
39 |
--------------------------------------------------------------------------------
/internal/openapi/style_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package openapi
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 | )
10 |
11 | func TestStyle_sanitize(t *testing.T) {
12 | a := assert.New(t, false)
13 |
14 | s := &Style{}
15 | a.Error(s.sanitize())
16 |
17 | s.Style = StyleDeepObject
18 | a.NotError(s.sanitize())
19 |
20 | s.Style = "invalid-value..."
21 | a.Error(s.sanitize())
22 | }
23 |
--------------------------------------------------------------------------------
/internal/xmlenc/token.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package xmlenc
4 |
5 | import "github.com/caixw/apidoc/v7/core"
6 |
7 | type (
8 | // Name 表示 XML 中的名称
9 | Name struct {
10 | core.Location
11 | Prefix String
12 | Local String
13 | }
14 |
15 | // StartElement 表示 XML 的元素
16 | StartElement struct {
17 | core.Location
18 | Name Name
19 | Attributes []*Attribute
20 | SelfClose bool // 是否自闭合
21 | }
22 |
23 | // EndElement XML 的结束元素
24 | EndElement struct {
25 | core.Location
26 | Name Name
27 | }
28 |
29 | // Instruction 表示 XML 的指令
30 | Instruction struct {
31 | core.Location
32 | Name String
33 | Attributes []*Attribute
34 | }
35 |
36 | // Attribute 表示 XML 属性
37 | Attribute struct {
38 | core.Location
39 | Name Name
40 | Value String
41 | }
42 |
43 | // String 表示 XML 的字符串数据
44 | String struct {
45 | core.Location
46 | Value string
47 | }
48 |
49 | // CData 表示 XML 的 CDATA 数据
50 | CData struct {
51 | BaseTag
52 | Value String
53 | }
54 |
55 | // Comment 表示 XML 的注释
56 | Comment struct {
57 | core.Location
58 | Value String
59 | }
60 | )
61 |
62 | // Match 是否与 end 相匹配
63 | func (s *StartElement) Match(end *EndElement) bool {
64 | return s.Name.Equal(end.Name)
65 | }
66 |
67 | // Equal 两个 name 是否相等
68 | func (n Name) Equal(v Name) bool {
69 | return n.Prefix.Value == v.Prefix.Value &&
70 | n.Local.Value == v.Local.Value
71 | }
72 |
73 | // String fmt.Stringer
74 | func (n Name) String() string {
75 | if n.Prefix.Value == "" {
76 | return n.Local.Value
77 | }
78 | return n.Prefix.Value + ":" + n.Local.Value
79 | }
80 |
--------------------------------------------------------------------------------
/internal/xmlenc/xmlenc.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // Package xmlenc 有关文档格式编解码处理
4 | package xmlenc
5 |
6 | import (
7 | "golang.org/x/text/message"
8 |
9 | "github.com/caixw/apidoc/v7/core"
10 | "github.com/caixw/apidoc/v7/internal/locale"
11 | )
12 |
13 | // Base 所有文档节点的基本元素
14 | type Base struct {
15 | core.Location
16 | UsageKey message.Reference `apidoc:"-"` // 表示对当前元素的一个说明内容的翻译 ID
17 | }
18 |
19 | // BaseAttribute 所有 XML 属性节点的基本元素
20 | type BaseAttribute struct {
21 | Base
22 | AttributeName Name `apidoc:"-"`
23 | }
24 |
25 | // BaseTag 所有 XML 标签的基本元素
26 | type BaseTag struct {
27 | Base
28 | StartTag Name `apidoc:"-"` // 表示起始标签名
29 | EndTag Name `apidoc:"-"` // 表示标签的结束名称,如果是自闭合的标签,此值的 Local.Value 为空。
30 | }
31 |
32 | // Usage 本地化的当前字段介绍内容
33 | func (b Base) Usage() string {
34 | if b.UsageKey == nil {
35 | return ""
36 | }
37 |
38 | return locale.Sprintf(b.UsageKey)
39 | }
40 |
41 | // SelfClose 当前是否为自闭合标签
42 | func (b *BaseTag) SelfClose() bool {
43 | return b.EndTag.Local.Value == ""
44 | }
45 |
--------------------------------------------------------------------------------
/internal/xmlenc/xmlenc_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | package xmlenc
4 |
5 | import (
6 | "testing"
7 |
8 | "github.com/issue9/assert/v3"
9 |
10 | "github.com/caixw/apidoc/v7/internal/locale"
11 | )
12 |
13 | func TestBaseTag_SelfClose(t *testing.T) {
14 | a := assert.New(t, false)
15 |
16 | b := &BaseTag{}
17 | a.True(b.SelfClose())
18 |
19 | b = &BaseTag{EndTag: Name{
20 | Prefix: String{
21 | Value: "name",
22 | },
23 | }}
24 | a.True(b.SelfClose())
25 |
26 | b.EndTag.Local = String{
27 | Value: "name",
28 | }
29 | a.False(b.SelfClose())
30 | }
31 |
32 | func TestBase_Usage(t *testing.T) {
33 | a := assert.New(t, false)
34 |
35 | b := Base{}
36 | a.Empty(b.Usage())
37 |
38 | b.UsageKey = locale.ErrInvalidUTF8Character
39 | a.Equal(b.Usage(), locale.Sprintf(locale.ErrInvalidUTF8Character))
40 | }
41 |
--------------------------------------------------------------------------------
/scripts/build-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # SPDX-License-Identifier: MIT
3 |
4 | # 指定工作目录
5 | wd=$(dirname $0)/../cmd/apidoc
6 |
7 | # 指定编译日期
8 | date=`date -u '+%Y%m%d'`
9 |
10 | # 获取最后一条前交的 hash
11 | hash=`git rev-parse HEAD`
12 |
13 | # 需要修改变量的地址,若为 main,则指接使用 main,而不是全地址
14 | path=github.com/caixw/apidoc/v7/core
15 |
16 | ldflags="-X ${path}.metadata=${date}.${hash}"
17 |
--------------------------------------------------------------------------------
/scripts/build.cmd:
--------------------------------------------------------------------------------
1 | :: SPDX-License-Identifier: MIT
2 |
3 | set wd=%~dp0\..\cmd\apidoc
4 |
5 | set mainPath=github.com\caixw\apidoc
6 |
7 | set varsPath=%mainPath%\v7\core
8 |
9 | set builddate=%date:~0,4%%date:~5,2%%date:~8,2%
10 |
11 | for /f "delims=" %%t in ('git rev-parse HEAD') do set hash=%%t
12 |
13 | echo compile
14 | go build -o %wd%\apidoc.exe -ldflags "-X %varsPath%.metadata=%builddate%.%hash%" -v %wd%
15 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # SPDX-License-Identifier: MIT
3 |
4 | source $(dirname $0)/build-env.sh
5 |
6 | cd ${wd}
7 |
8 | echo '开始编译'
9 | go build -o ./apidoc -ldflags "${ldflags}" -v
10 |
--------------------------------------------------------------------------------
/scripts/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # SPDX-License-Identifier: MIT
3 |
4 | source $(dirname $0)/build-env.sh
5 |
6 | cd ${wd}
7 |
8 | echo '开始编译'
9 | go install -ldflags "${ldflags}" -v
10 |
--------------------------------------------------------------------------------