mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-14 05:56:14 +01:00
[API] Forgejo API /api/forgejo/v1
This commit is contained in:
parent
af47016bad
commit
82edd2421c
14 changed files with 400 additions and 2 deletions
32
Makefile
32
Makefile
|
@ -96,7 +96,10 @@ else
|
|||
endif
|
||||
endif
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||
# SemVer
|
||||
FORGEJO_VERSION := v2.0.0
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)"
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
|
||||
|
||||
|
@ -147,6 +150,8 @@ ifdef DEPS_PLAYWRIGHT
|
|||
PLAYWRIGHT_FLAGS += --with-deps
|
||||
endif
|
||||
|
||||
FORGEJO_API_SPEC := public/forgejo/api.v1.yml
|
||||
|
||||
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
||||
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g
|
||||
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g
|
||||
|
@ -207,6 +212,8 @@ help:
|
|||
@echo " - generate-license update license files"
|
||||
@echo " - generate-gitignore update gitignore files"
|
||||
@echo " - generate-manpage generate manpage"
|
||||
@echo " - generate-forgejo-api generate the forgejo API from spec"
|
||||
@echo " - forgejo-api-validate check if the forgejo API matches the specs"
|
||||
@echo " - generate-swagger generate the swagger spec from code comments"
|
||||
@echo " - swagger-validate check if the swagger spec is valid"
|
||||
@echo " - golangci-lint run golangci-lint linter"
|
||||
|
@ -296,6 +303,27 @@ ifneq "$(TAGS)" "$(shell cat $(TAGS_EVIDENCE) 2>/dev/null)"
|
|||
TAGS_PREREQ := $(TAGS_EVIDENCE)
|
||||
endif
|
||||
|
||||
OAPI_CODEGEN_PACKAGE ?= github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.12.4
|
||||
KIN_OPENAPI_CODEGEN_PACKAGE ?= github.com/getkin/kin-openapi/cmd/validate@v0.114.0
|
||||
FORGEJO_API_SERVER = routers/api/forgejo/v1/generated.go
|
||||
|
||||
.PHONY: generate-forgejo-api
|
||||
generate-forgejo-api: $(FORGEJO_API_SPEC)
|
||||
$(GO) run $(OAPI_CODEGEN_PACKAGE) -package v1 -generate chi-server,types $< > $(FORGEJO_API_SERVER)
|
||||
|
||||
.PHONY: forgejo-api-check
|
||||
forgejo-api-check: generate-forgejo-api
|
||||
@diff=$$(git diff $(FORGEJO_API_SERVER) ; \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make generate-forgejo-api' and commit the result:"; \
|
||||
echo "$${diff}"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: forgejo-api-validate
|
||||
forgejo-api-validate:
|
||||
$(GO) run $(KIN_OPENAPI_CODEGEN_PACKAGE) $(FORGEJO_API_SPEC)
|
||||
|
||||
.PHONY: generate-swagger
|
||||
generate-swagger: $(SWAGGER_SPEC)
|
||||
|
||||
|
@ -331,7 +359,7 @@ checks: checks-frontend checks-backend
|
|||
checks-frontend: lockfile-check svg-check
|
||||
|
||||
.PHONY: checks-backend
|
||||
checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate
|
||||
checks-backend: tidy-check swagger-check fmt-check misspell-check forgejo-api-validate swagger-validate
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-frontend lint-backend
|
||||
|
|
40
public/forgejo/api.v1.yml
Normal file
40
public/forgejo/api.v1.yml
Normal file
|
@ -0,0 +1,40 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Forgejo API
|
||||
description: |-
|
||||
Forgejo REST API
|
||||
|
||||
contact:
|
||||
email: contact@forgejo.org
|
||||
license:
|
||||
name: MIT
|
||||
url: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/LICENSE
|
||||
version: 1.0.0
|
||||
externalDocs:
|
||||
description: Find out more about Forgejo
|
||||
url: http://forgejo.org
|
||||
servers:
|
||||
- url: /api/forgejo/v1
|
||||
paths:
|
||||
/version:
|
||||
get:
|
||||
summary: API version
|
||||
description: Semantic version of the Forgejo API
|
||||
operationId: getVersion
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Version'
|
||||
components:
|
||||
schemas:
|
||||
Version:
|
||||
type: object
|
||||
properties:
|
||||
number:
|
||||
type: string
|
||||
|
18
routers/api/forgejo/v1/api.go
Normal file
18
routers/api/forgejo/v1/api.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
)
|
||||
|
||||
func Routes(ctx gocontext.Context) *web.Route {
|
||||
m := web.NewRoute()
|
||||
forgejo := NewForgejo()
|
||||
m.Get("", Root)
|
||||
m.Get("/version", forgejo.GetVersion)
|
||||
return m
|
||||
}
|
24
routers/api/forgejo/v1/forgejo.go
Normal file
24
routers/api/forgejo/v1/forgejo.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
type Forgejo struct{}
|
||||
|
||||
var _ ServerInterface = &Forgejo{}
|
||||
|
||||
func NewForgejo() *Forgejo {
|
||||
return &Forgejo{}
|
||||
}
|
||||
|
||||
var ForgejoVersion = "development"
|
||||
|
||||
func (f *Forgejo) GetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(Version{&ForgejoVersion})
|
||||
}
|
167
routers/api/forgejo/v1/generated.go
Normal file
167
routers/api/forgejo/v1/generated.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
// Package v1 provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/deepmap/oapi-codegen version v1.12.4 DO NOT EDIT.
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// Version defines model for Version.
|
||||
type Version struct {
|
||||
Number *string `json:"number,omitempty"`
|
||||
}
|
||||
|
||||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
// API version
|
||||
// (GET /version)
|
||||
GetVersion(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// ServerInterfaceWrapper converts contexts to parameters.
|
||||
type ServerInterfaceWrapper struct {
|
||||
Handler ServerInterface
|
||||
HandlerMiddlewares []MiddlewareFunc
|
||||
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
|
||||
}
|
||||
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
// GetVersion operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.GetVersion(w, r)
|
||||
})
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
|
||||
type UnescapedCookieParamError struct {
|
||||
ParamName string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *UnescapedCookieParamError) Error() string {
|
||||
return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName)
|
||||
}
|
||||
|
||||
func (e *UnescapedCookieParamError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type UnmarshallingParamError struct {
|
||||
ParamName string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *UnmarshallingParamError) Error() string {
|
||||
return fmt.Sprintf("Error unmarshalling parameter %s as JSON: %s", e.ParamName, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *UnmarshallingParamError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type RequiredParamError struct {
|
||||
ParamName string
|
||||
}
|
||||
|
||||
func (e *RequiredParamError) Error() string {
|
||||
return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName)
|
||||
}
|
||||
|
||||
type RequiredHeaderError struct {
|
||||
ParamName string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *RequiredHeaderError) Error() string {
|
||||
return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName)
|
||||
}
|
||||
|
||||
func (e *RequiredHeaderError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type InvalidParamFormatError struct {
|
||||
ParamName string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *InvalidParamFormatError) Error() string {
|
||||
return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *InvalidParamFormatError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type TooManyValuesForParamError struct {
|
||||
ParamName string
|
||||
Count int
|
||||
}
|
||||
|
||||
func (e *TooManyValuesForParamError) Error() string {
|
||||
return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count)
|
||||
}
|
||||
|
||||
// Handler creates http.Handler with routing matching OpenAPI spec.
|
||||
func Handler(si ServerInterface) http.Handler {
|
||||
return HandlerWithOptions(si, ChiServerOptions{})
|
||||
}
|
||||
|
||||
type ChiServerOptions struct {
|
||||
BaseURL string
|
||||
BaseRouter chi.Router
|
||||
Middlewares []MiddlewareFunc
|
||||
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
|
||||
}
|
||||
|
||||
// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
|
||||
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
|
||||
return HandlerWithOptions(si, ChiServerOptions{
|
||||
BaseRouter: r,
|
||||
})
|
||||
}
|
||||
|
||||
func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler {
|
||||
return HandlerWithOptions(si, ChiServerOptions{
|
||||
BaseURL: baseURL,
|
||||
BaseRouter: r,
|
||||
})
|
||||
}
|
||||
|
||||
// HandlerWithOptions creates http.Handler with additional options
|
||||
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
|
||||
r := options.BaseRouter
|
||||
|
||||
if r == nil {
|
||||
r = chi.NewRouter()
|
||||
}
|
||||
if options.ErrorHandlerFunc == nil {
|
||||
options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
wrapper := ServerInterfaceWrapper{
|
||||
Handler: si,
|
||||
HandlerMiddlewares: options.Middlewares,
|
||||
ErrorHandlerFunc: options.ErrorHandlerFunc,
|
||||
}
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Get(options.BaseURL+"/version", wrapper.GetVersion)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
14
routers/api/forgejo/v1/root.go
Normal file
14
routers/api/forgejo/v1/root.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright The Forgejo Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Root(w http.ResponseWriter, r *http.Request) {
|
||||
// https://www.rfc-editor.org/rfc/rfc8631
|
||||
w.Header().Set("Link", "</assets/forgejo/api.v1.yml>; rel=\"service-desc\"")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
|
@ -31,6 +31,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
forgejo "code.gitea.io/gitea/routers/api/forgejo/v1"
|
||||
packages_router "code.gitea.io/gitea/routers/api/packages"
|
||||
apiv1 "code.gitea.io/gitea/routers/api/v1"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
|
@ -184,6 +185,7 @@ func NormalRoutes(ctx context.Context) *web.Route {
|
|||
|
||||
r.Mount("/", web_routers.Routes(ctx))
|
||||
r.Mount("/api/v1", apiv1.Routes(ctx))
|
||||
r.Mount("/api/forgejo/v1", forgejo.Routes(ctx))
|
||||
r.Mount("/api/internal", private.Routes())
|
||||
if setting.Packages.Enabled {
|
||||
r.Mount("/api/packages", packages_router.Routes(ctx))
|
||||
|
|
19
routers/web/misc/swagger-forgejo.go
Normal file
19
routers/web/misc/swagger-forgejo.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package misc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
)
|
||||
|
||||
// tplSwagger swagger page template
|
||||
const tplForgejoSwagger base.TplName = "swagger/forgejo-ui"
|
||||
|
||||
func SwaggerForgejo(ctx *context.Context) {
|
||||
ctx.Data["APIVersion"] = "v1"
|
||||
ctx.HTML(http.StatusOK, tplForgejoSwagger)
|
||||
}
|
|
@ -211,6 +211,7 @@ func Routes(ctx gocontext.Context) *web.Route {
|
|||
if setting.API.EnableSwagger {
|
||||
// Note: The route moved from apiroutes because it's in fact want to render a web page
|
||||
routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default
|
||||
routes.Get("/api/forgejo/swagger", append(common, misc.SwaggerForgejo)...)
|
||||
}
|
||||
|
||||
// TODO: These really seem like things that could be folded into Contexter or as helper functions
|
||||
|
|
13
templates/swagger/forgejo-ui.tmpl
Normal file
13
templates/swagger/forgejo-ui.tmpl
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Forgejo API</title>
|
||||
<link href="{{AssetUrlPrefix}}/css/swagger.css?v={{AssetVersion}}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<a class="swagger-back-link" href="{{AppUrl}}">{{svg "octicon-reply"}}{{.locale.Tr "return_to_gitea"}}</a>
|
||||
<div id="swagger-ui" data-source="{{AssetUrlPrefix}}/forgejo/api.{{.APIVersion}}.yml"></div>
|
||||
<script src="{{AssetUrlPrefix}}/js/forgejoswagger.js?v={{AssetVersion}}"></script>
|
||||
</body>
|
||||
</html>
|
21
tests/integration/api_forgejo_root_test.go
Normal file
21
tests/integration/api_forgejo_root_test.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright The Forgejo Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIForgejoRoot(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/forgejo/v1")
|
||||
resp := MakeRequest(t, req, http.StatusNoContent)
|
||||
assert.Contains(t, resp.Header().Get("Link"), "/assets/forgejo/api.v1.yml")
|
||||
}
|
25
tests/integration/api_forgejo_version_test.go
Normal file
25
tests/integration/api_forgejo_version_test.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright The Forgejo Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/routers/api/forgejo/v1"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIForgejoVersion(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/forgejo/v1/version")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var version v1.Version
|
||||
DecodeJSON(t, resp, &version)
|
||||
assert.Equal(t, "development", *version.Number)
|
||||
}
|
22
web_src/js/standalone/forgejo-swagger.js
Normal file
22
web_src/js/standalone/forgejo-swagger.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js';
|
||||
import 'swagger-ui-dist/swagger-ui.css';
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
const url = document.getElementById('swagger-ui').getAttribute('data-source');
|
||||
|
||||
const ui = SwaggerUI({
|
||||
url: url,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
docExpansion: 'none',
|
||||
defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
|
||||
presets: [
|
||||
SwaggerUI.presets.apis
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUI.plugins.DownloadUrl
|
||||
]
|
||||
});
|
||||
|
||||
window.ui = ui;
|
||||
});
|
|
@ -58,6 +58,10 @@ export default {
|
|||
fileURLToPath(new URL('web_src/fomantic/build/semantic.css', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/less/index.less', import.meta.url)),
|
||||
],
|
||||
forgejoswagger: [ // Forgejo swagger is OpenAPI 3.0.0 and has specific parameters
|
||||
fileURLToPath(new URL('web_src/js/standalone/forgejo-swagger.js', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/less/standalone/swagger.less', import.meta.url)),
|
||||
],
|
||||
swagger: [
|
||||
fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)),
|
||||
fileURLToPath(new URL('web_src/less/standalone/swagger.less', import.meta.url)),
|
||||
|
|
Loading…
Reference in a new issue