[API] Forgejo API /api/forgejo/v1

(cherry picked from commit 20b5669269)
(cherry picked from commit 1574643a6a)

Update semantic version according to specification

(cherry picked from commit 22510f4130)

Mise à jour de 'Makefile'

(cherry picked from commit c3d85d8409)
(cherry picked from commit 5ea2309851)
(cherry picked from commit 4f3970e6c4)

[API] [SEMVER] replace number with version

[API] [SEMVER] [v1.20] less is replaced by css

(cherry picked from commit 43a3a40825)
(cherry picked from commit 669cea25bb)
(cherry picked from commit e25190d2b4)
(cherry picked from commit 5df876e19e)
(cherry picked from commit fc94f6fae2)
(cherry picked from commit 58c50c1fe4)
This commit is contained in:
Earl Warren 2023-02-07 11:23:49 +01:00
parent dec8777918
commit 80ac107dc9
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
14 changed files with 401 additions and 3 deletions

View file

@ -102,7 +102,10 @@ ifeq ($(VERSION),main)
VERSION := main-nightly VERSION := main-nightly
endif endif
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" # SemVer
FORGEJO_VERSION := 3.0.0+0-gitea-1.19.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 LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
@ -154,6 +157,8 @@ ifdef DEPS_PLAYWRIGHT
PLAYWRIGHT_FLAGS += --with-deps PLAYWRIGHT_FLAGS += --with-deps
endif endif
FORGEJO_API_SPEC := public/forgejo/api.v1.yml
SWAGGER_SPEC := templates/swagger/v1_json.tmpl 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_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 SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g
@ -214,6 +219,8 @@ help:
@echo " - generate-license update license files" @echo " - generate-license update license files"
@echo " - generate-gitignore update gitignore files" @echo " - generate-gitignore update gitignore files"
@echo " - generate-manpage generate manpage" @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 " - generate-swagger generate the swagger spec from code comments"
@echo " - swagger-validate check if the swagger spec is valid" @echo " - swagger-validate check if the swagger spec is valid"
@echo " - golangci-lint run golangci-lint linter" @echo " - golangci-lint run golangci-lint linter"
@ -303,6 +310,27 @@ ifneq "$(TAGS)" "$(shell cat $(TAGS_EVIDENCE) 2>/dev/null)"
TAGS_PREREQ := $(TAGS_EVIDENCE) TAGS_PREREQ := $(TAGS_EVIDENCE)
endif 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 .PHONY: generate-swagger
generate-swagger: $(SWAGGER_SPEC) generate-swagger: $(SWAGGER_SPEC)
@ -338,7 +366,7 @@ checks: checks-frontend checks-backend
checks-frontend: lockfile-check svg-check checks-frontend: lockfile-check svg-check
.PHONY: checks-backend .PHONY: checks-backend
checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate security-check checks-backend: tidy-check swagger-check fmt-check misspell-check forgejo-api-validate swagger-validate security-check
.PHONY: lint .PHONY: lint
lint: lint-frontend lint-backend lint: lint-frontend lint-backend

40
public/forgejo/api.v1.yml Normal file
View 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:
version:
type: string

View 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
}

View 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})
}

View 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 {
Version *string `json:"version,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
}

View 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)
}

View file

@ -31,6 +31,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
actions_router "code.gitea.io/gitea/routers/api/actions" actions_router "code.gitea.io/gitea/routers/api/actions"
forgejo "code.gitea.io/gitea/routers/api/forgejo/v1"
packages_router "code.gitea.io/gitea/routers/api/packages" packages_router "code.gitea.io/gitea/routers/api/packages"
apiv1 "code.gitea.io/gitea/routers/api/v1" apiv1 "code.gitea.io/gitea/routers/api/v1"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
@ -190,6 +191,7 @@ func NormalRoutes(ctx context.Context) *web.Route {
r.Mount("/", web_routers.Routes(ctx)) r.Mount("/", web_routers.Routes(ctx))
r.Mount("/api/v1", apiv1.Routes(ctx)) r.Mount("/api/v1", apiv1.Routes(ctx))
r.Mount("/api/forgejo/v1", forgejo.Routes(ctx))
r.Mount("/api/internal", private.Routes()) r.Mount("/api/internal", private.Routes())
if setting.Packages.Enabled { if setting.Packages.Enabled {

View 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)
}

View file

@ -212,6 +212,7 @@ func Routes(ctx gocontext.Context) *web.Route {
if setting.API.EnableSwagger { if setting.API.EnableSwagger {
// Note: The route moved from apiroutes because it's in fact want to render a web page // 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/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 // TODO: These really seem like things that could be folded into Contexter or as helper functions

View 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>

View 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")
}

View 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.Version)
}

View 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;
});

View file

@ -62,6 +62,10 @@ export default {
webcomponents: [ webcomponents: [
fileURLToPath(new URL('web_src/js/webcomponents/GiteaOriginUrl.js', import.meta.url)), fileURLToPath(new URL('web_src/js/webcomponents/GiteaOriginUrl.js', 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/css/standalone/swagger.css', import.meta.url)),
],
swagger: [ swagger: [
fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)), fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)),
fileURLToPath(new URL('web_src/css/standalone/swagger.css', import.meta.url)), fileURLToPath(new URL('web_src/css/standalone/swagger.css', import.meta.url)),