Merge pull request 'Generate client code based on the Swagger > OpenApi document' (#13) from features/generate-client-api into main

Reviewed-on: #13
This commit is contained in:
jtom38 2024-07-17 22:04:34 -07:00
commit 1b36ad9040
16 changed files with 4330 additions and 473 deletions

2
.gitignore vendored
View File

@ -3,6 +3,7 @@ dev.session.sql
__debug_bin
server
.vscode
openapi.json
# Binaries for programs and plugins
*.exe
@ -12,6 +13,7 @@ server
*.dylib
collector
newsbot.db
tmp/
# Test binary, built with `go test -c`
*.test

3756
api/api.gen.go Normal file

File diff suppressed because it is too large Load Diff

6
api/client.yaml Normal file
View File

@ -0,0 +1,6 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/deepmap/oapi-codegen/HEAD/configuration-schema.json
package: api
output: api.gen.go
generate:
models: true
client: true

View File

@ -32,10 +32,11 @@ const docTemplate = `{
"summary": "Lists the top 25 records ordering from newest to oldest.",
"parameters": [
{
"type": "string",
"type": "integer",
"description": "page number",
"name": "page",
"in": "query"
"in": "query",
"required": true
}
],
"responses": {
@ -60,7 +61,7 @@ const docTemplate = `{
}
}
},
"/v1/articles/by/sourceId": {
"/v1/articles/by/source/{id}": {
"get": {
"security": [
{
@ -79,7 +80,7 @@ const docTemplate = `{
"type": "string",
"description": "source id",
"name": "id",
"in": "query",
"in": "path",
"required": true
},
{
@ -111,7 +112,7 @@ const docTemplate = `{
}
}
},
"/v1/articles/{ID}": {
"/v1/articles/{id}": {
"get": {
"security": [
{
@ -129,7 +130,7 @@ const docTemplate = `{
{
"type": "string",
"description": "int",
"name": "ID",
"name": "id",
"in": "path",
"required": true
}
@ -156,7 +157,7 @@ const docTemplate = `{
}
}
},
"/v1/articles/{ID}/details": {
"/v1/articles/{id}/details": {
"get": {
"security": [
{
@ -174,7 +175,7 @@ const docTemplate = `{
{
"type": "string",
"description": "int",
"name": "ID",
"name": "id",
"in": "path",
"required": true
}
@ -345,7 +346,50 @@ const docTemplate = `{
}
}
},
"/v1/discord/webhooks/{ID}": {
"/v1/discord/webhooks/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"DiscordWebhook"
],
"summary": "Returns the top 100 entries from the queue to be processed.",
"parameters": [
{
"type": "integer",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
}
}
},
"delete": {
"security": [
{
@ -387,7 +431,7 @@ const docTemplate = `{
}
}
},
"/v1/discord/webhooks/{ID}/disable": {
"/v1/discord/webhooks/{id}/disable": {
"post": {
"security": [
{
@ -429,7 +473,7 @@ const docTemplate = `{
}
}
},
"/v1/discord/webhooks/{ID}/enable": {
"/v1/discord/webhooks/{id}/enable": {
"post": {
"security": [
{
@ -471,51 +515,6 @@ const docTemplate = `{
}
}
},
"/v1/discord/webhooks/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"DiscordWebhook"
],
"summary": "Returns the top 100 entries from the queue to be processed.",
"parameters": [
{
"type": "integer",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
}
}
}
},
"/v1/sources": {
"get": {
"security": [
@ -1512,24 +1511,16 @@ const docTemplate = `{
}
}
}
},
"securityDefinitions": {
"Bearer": {
"description": "Type \"Bearer\" followed by a space and JWT token.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "0.1",
Version: "",
Host: "",
BasePath: "/api",
BasePath: "",
Schemes: []string{},
Title: "NewsBot collector",
Title: "",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,

View File

@ -1,11 +1,8 @@
{
"swagger": "2.0",
"info": {
"title": "NewsBot collector",
"contact": {},
"version": "0.1"
"contact": {}
},
"basePath": "/api",
"paths": {
"/v1/articles": {
"get": {
@ -23,10 +20,11 @@
"summary": "Lists the top 25 records ordering from newest to oldest.",
"parameters": [
{
"type": "string",
"type": "integer",
"description": "page number",
"name": "page",
"in": "query"
"in": "query",
"required": true
}
],
"responses": {
@ -51,7 +49,7 @@
}
}
},
"/v1/articles/by/sourceId": {
"/v1/articles/by/source/{id}": {
"get": {
"security": [
{
@ -70,7 +68,7 @@
"type": "string",
"description": "source id",
"name": "id",
"in": "query",
"in": "path",
"required": true
},
{
@ -102,7 +100,7 @@
}
}
},
"/v1/articles/{ID}": {
"/v1/articles/{id}": {
"get": {
"security": [
{
@ -120,7 +118,7 @@
{
"type": "string",
"description": "int",
"name": "ID",
"name": "id",
"in": "path",
"required": true
}
@ -147,7 +145,7 @@
}
}
},
"/v1/articles/{ID}/details": {
"/v1/articles/{id}/details": {
"get": {
"security": [
{
@ -165,7 +163,7 @@
{
"type": "string",
"description": "int",
"name": "ID",
"name": "id",
"in": "path",
"required": true
}
@ -336,7 +334,50 @@
}
}
},
"/v1/discord/webhooks/{ID}": {
"/v1/discord/webhooks/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"DiscordWebhook"
],
"summary": "Returns the top 100 entries from the queue to be processed.",
"parameters": [
{
"type": "integer",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
}
}
},
"delete": {
"security": [
{
@ -378,7 +419,7 @@
}
}
},
"/v1/discord/webhooks/{ID}/disable": {
"/v1/discord/webhooks/{id}/disable": {
"post": {
"security": [
{
@ -420,7 +461,7 @@
}
}
},
"/v1/discord/webhooks/{ID}/enable": {
"/v1/discord/webhooks/{id}/enable": {
"post": {
"security": [
{
@ -462,51 +503,6 @@
}
}
},
"/v1/discord/webhooks/{id}": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"DiscordWebhook"
],
"summary": "Returns the top 100 entries from the queue to be processed.",
"parameters": [
{
"type": "integer",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.DiscordWebhookResponse"
}
}
}
}
},
"/v1/sources": {
"get": {
"security": [
@ -1503,13 +1499,5 @@
}
}
}
},
"securityDefinitions": {
"Bearer": {
"description": "Type \"Bearer\" followed by a space and JWT token.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}

View File

@ -1,4 +1,3 @@
basePath: /api
definitions:
domain.ArticleAndSourceModel:
properties:
@ -154,8 +153,6 @@ definitions:
type: object
info:
contact: {}
title: NewsBot collector
version: "0.1"
paths:
/v1/articles:
get:
@ -163,7 +160,8 @@ paths:
- description: page number
in: query
name: page
type: string
required: true
type: integer
produces:
- application/json
responses:
@ -184,12 +182,12 @@ paths:
summary: Lists the top 25 records ordering from newest to oldest.
tags:
- Articles
/v1/articles/{ID}:
/v1/articles/{id}:
get:
parameters:
- description: int
in: path
name: ID
name: id
required: true
type: string
produces:
@ -212,12 +210,12 @@ paths:
summary: Returns an article based on defined ID.
tags:
- Articles
/v1/articles/{ID}/details:
/v1/articles/{id}/details:
get:
parameters:
- description: int
in: path
name: ID
name: id
required: true
type: string
produces:
@ -240,11 +238,11 @@ paths:
summary: Returns an article and source based on defined ID.
tags:
- Articles
/v1/articles/by/sourceId:
/v1/articles/by/source/{id}:
get:
parameters:
- description: source id
in: query
in: path
name: id
required: true
type: string
@ -295,7 +293,7 @@ paths:
summary: Returns the top 100
tags:
- DiscordWebhook
/v1/discord/webhooks/{ID}:
/v1/discord/webhooks/{id}:
delete:
parameters:
- description: id
@ -321,59 +319,6 @@ paths:
summary: Deletes a record by ID.
tags:
- DiscordWebhook
/v1/discord/webhooks/{ID}/disable:
post:
parameters:
- description: id
in: path
name: id
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
security:
- Bearer: []
summary: Disables a Webhook from being used.
tags:
- DiscordWebhook
/v1/discord/webhooks/{ID}/enable:
post:
parameters:
- description: id
in: path
name: id
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
security:
- Bearer: []
summary: Enables a source to continue processing.
tags:
- DiscordWebhook
/v1/discord/webhooks/{id}:
get:
parameters:
- description: id
@ -401,6 +346,58 @@ paths:
summary: Returns the top 100 entries from the queue to be processed.
tags:
- DiscordWebhook
/v1/discord/webhooks/{id}/disable:
post:
parameters:
- description: id
in: path
name: id
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
security:
- Bearer: []
summary: Disables a Webhook from being used.
tags:
- DiscordWebhook
/v1/discord/webhooks/{id}/enable:
post:
parameters:
- description: id
in: path
name: id
required: true
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.DiscordWebhookResponse'
security:
- Bearer: []
summary: Enables a source to continue processing.
tags:
- DiscordWebhook
/v1/discord/webhooks/by/serverAndChannel:
get:
parameters:
@ -945,10 +942,4 @@ paths:
summary: Adds a new scope to a user account
tags:
- Users
securityDefinitions:
Bearer:
description: Type "Bearer" followed by a space and JWT token.
in: header
name: Authorization
type: apiKey
swagger: "2.0"

View File

@ -12,7 +12,7 @@ import (
// ListArticles
// @Summary Lists the top 25 records ordering from newest to oldest.
// @Produce application/json
// @Param page query string false "page number"
// @Param page query int true "page number" test
// @Tags Articles
// @Router /v1/articles [get]
// @Success 200 {object} domain.ArticleResponse
@ -49,10 +49,10 @@ func (s *Handler) listArticles(c echo.Context) error {
// GetArticle
// @Summary Returns an article based on defined ID.
// @Param ID path string true "int"
// @Param id path string true "int"
// @Produce application/json
// @Tags Articles
// @Router /v1/articles/{ID} [get]
// @Router /v1/articles/{id} [get]
// @Success 200 {object} domain.ArticleResponse "OK"
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
@ -91,10 +91,10 @@ func (s *Handler) getArticle(c echo.Context) error {
// GetArticleDetails
// @Summary Returns an article and source based on defined ID.
// @Param ID path string true "int"
// @Param id path string true "int"
// @Produce application/json
// @Tags Articles
// @Router /v1/articles/{ID}/details [get]
// @Router /v1/articles/{id}/details [get]
// @Success 200 {object} domain.ArticleDetailedResponse "OK"
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
@ -136,11 +136,11 @@ func (s *Handler) getArticleDetails(c echo.Context) error {
// ListArticlesBySourceID
// @Summary Finds the articles based on the SourceID provided. Returns the top 25.
// @Param id query string true "source id"
// @Param id path string true "source id"
// @Param page query int false "Page to query"
// @Produce application/json
// @Tags Articles
// @Router /v1/articles/by/sourceId [get]
// @Router /v1/articles/by/source/{id} [get]
// @Success 200 {object} domain.ArticleResponse "OK"
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse

View File

@ -211,7 +211,7 @@ func (s *Handler) NewDiscordWebHook(c echo.Context) error {
// @Summary Disables a Webhook from being used.
// @Param id path int true "id"
// @Tags DiscordWebhook
// @Router /v1/discord/webhooks/{ID}/disable [post]
// @Router /v1/discord/webhooks/{id}/disable [post]
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.DiscordWebhookResponse
@ -279,7 +279,7 @@ func (s *Handler) disableDiscordWebHook(c echo.Context) error {
// @Summary Enables a source to continue processing.
// @Param id path int true "id"
// @Tags DiscordWebhook
// @Router /v1/discord/webhooks/{ID}/enable [post]
// @Router /v1/discord/webhooks/{id}/enable [post]
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.DiscordWebhookResponse
@ -345,7 +345,7 @@ func (s *Handler) enableDiscordWebHook(c echo.Context) error {
// @Summary Deletes a record by ID.
// @Param id path string true "id"
// @Tags DiscordWebhook
// @Router /v1/discord/webhooks/{ID} [delete]
// @Router /v1/discord/webhooks/{id} [delete]
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.DiscordWebhookResponse

View File

@ -8,6 +8,7 @@ import (
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/dtoconv"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/labstack/echo/v4"
)
@ -419,7 +420,8 @@ func (s *Handler) newRssSource(c echo.Context) error {
}
param := domain.NewSourceParamRequest{}
err = (&echo.DefaultBinder{}).BindBody(c, &param)
err = c.Bind(&param)
//err = (&echo.DefaultBinder{}).BindBody(c, &param)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
@ -431,8 +433,15 @@ func (s *Handler) newRssSource(c echo.Context) error {
}
// Check if the record already exists
blankRecord := entity.SourceEntity{}
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorRss, param.Name)
if err == nil {
if err != nil {
p.IsError = true
return c.JSON(http.StatusInternalServerError, p)
}
// No record was found, but no error returned
if item != blankRecord {
var dto []domain.SourceDto
dto = append(dto, dtoconv.SourceToDto(item))
p.Payload = dto
@ -440,6 +449,14 @@ func (s *Handler) newRssSource(c echo.Context) error {
return c.JSON(http.StatusOK, p)
}
//if err == nil {
// var dto []domain.SourceDto
// dto = append(dto, dtoconv.SourceToDto(item))
// p.Payload = dto
// p.BaseResponse.IsError = false
// return c.JSON(http.StatusOK, p)
//}
tags := fmt.Sprintf("rss, %v, %s", param.Name, param.Tags)
rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorRss, param.Name, param.Url, tags, true)
if err != nil {

View File

@ -121,7 +121,7 @@ func (r sourceRepository) GetBySourceAndName(ctx context.Context, source, name s
b.Select("*")
b.From("Sources").Where(
b.Equal("Source", source),
b.Equal("Name", name),
b.Equal("DisplayName", name),
)
b.Limit(1)
query, args := b.Build()

View File

@ -19,8 +19,13 @@ migrate-dev-down: ## revert sql migrations to dev db
goose -dir "./internal/database/migrations" sqlite3 ./cmd/newsbot.db down
swag: ## Generates the swagger documentation with the swag tool
~/go/bin/swag f
~/go/bin/swag f -g cmd/server.go
~/go/bin/swag init -g cmd/server.go
go run tools/swaggertoopenapi/main.go
oapi-codegen -config api/client.yaml docs/openapi.json
mv api.gen.go api/
gensql: ## Generates SQL code with sqlc
sqlc generate
install-tools: ## Installs the required tools for this project
go install github.com/swaggo/swag/cmd/swag@v1.8.1
go install github.com/pressly/goose/v3/cmd/goose@latest
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.3.0

View File

@ -0,0 +1,17 @@
package main
import (
"fmt"
"os"
"git.jamestombleson.com/jtom38/newsbot-api/tools/swaggertoopenapi/src"
)
func main() {
err := src.ConvertToOpenApi("docs/swagger.json", "docs/openapi.json", true)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -0,0 +1,70 @@
package src
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
)
const (
ApplicationJson = "application/json"
)
func ConvertToOpenApi(filePath, outputPath string, force bool) error {
// check if the file already exists
exists, err := os.Stat(outputPath)
if exists != nil {
// if force was given, attempt to remove it
if force {
err = os.Remove(outputPath)
if err != nil {
return err
}
} else {
return fmt.Errorf("'%s' already exists and force was not approved", outputPath)
}
}
log.Printf("Reading '%s'", filePath)
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
client := http.Client{}
req, err := http.NewRequest(http.MethodPost, "https://converter.swagger.io/api/convert", bytes.NewReader(content))
if err != nil {
return err
}
req.Header.Add("Accept", ApplicationJson)
req.Header.Add("Content-Type", ApplicationJson)
log.Println("Converting to OpenAPI spec")
resp, err := client.Do(req)
if err != nil {
return err
}
log.Println("Reading the response")
respContent, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
log.Println("Writing converted data to openapi.json")
writer, err := os.Create(outputPath)
if err != nil {
return err
}
defer writer.Close()
_, err = writer.Write(respContent)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,14 @@
package src_test
import (
"testing"
"git.jamestombleson.com/jtom38/newsbot-api/tools/swaggerToOpenapi/src"
)
func TestConvertOnline(t *testing.T) {
err := src.ConvertToOpenApi("../../docs/swagger.json", "../../docs/openapi.json", true)
if err != nil {
t.FailNow()
}
}