mirror of
https://github.com/bbernhard/signal-cli-rest-api.git
synced 2026-05-18 13:24:15 +00:00
Merge pull request #842 from Gara-Dorta/receive-swagger
Add swagger definitions for the receive end point
This commit is contained in:
commit
5c0bd056a7
15
Dockerfile
15
Dockerfile
@ -92,10 +92,21 @@ COPY src/main.go /tmp/signal-cli-rest-api-src/
|
||||
COPY src/go.mod /tmp/signal-cli-rest-api-src/
|
||||
COPY src/go.sum /tmp/signal-cli-rest-api-src/
|
||||
COPY src/plugin_loader.go /tmp/signal-cli-rest-api-src/
|
||||
COPY src/docs/add_v1_receive_schemas.go /tmp/signal-cli-rest-api-src/docs/add_v1_receive_schemas.go
|
||||
|
||||
RUN ls -la /tmp/signal-cli-rest-api-src
|
||||
|
||||
# build the docs
|
||||
RUN cd /tmp/signal-cli-rest-api-src && ${GOPATH}/bin/swag init --requiredByDefault --outputTypes "go,json"
|
||||
|
||||
# manually add the json schemas for the receive V1 endpoint to the docs
|
||||
RUN cd /tmp/signal-cli-rest-api-src/docs \
|
||||
&& wget https://github.com/Gara-Dorta/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz \
|
||||
&& mkdir signal-cli-schemas \
|
||||
&& tar xf signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz -C signal-cli-schemas \
|
||||
&& go run add_v1_receive_schemas.go signal-cli-schemas
|
||||
|
||||
# build signal-cli-rest-api
|
||||
RUN ls -la /tmp/signal-cli-rest-api-src
|
||||
RUN cd /tmp/signal-cli-rest-api-src && ${GOPATH}/bin/swag init --requiredByDefault
|
||||
RUN cd /tmp/signal-cli-rest-api-src && go build -o signal-cli-rest-api main.go
|
||||
RUN cd /tmp/signal-cli-rest-api-src && go test ./client -v && go test ./utils -v
|
||||
|
||||
|
||||
@ -4,44 +4,64 @@ These files are generated using the [swaggo/swag](https://github.com/swaggo/swag
|
||||
|
||||
There are two steps, first generating the docs and then running the web server.
|
||||
|
||||
## Generating the docs
|
||||
## With docker compose (recommended)
|
||||
|
||||
Regenerate the files with your local source code changes.
|
||||
1. Build the docs
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
2. Serve the docs
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
3. Go to http://127.0.0.1:8080/swagger/index.html to view the docs
|
||||
* If you get a Network error, replace the IP for the docker internal IP in the error, e.g: http://172.18.0.2:8080/swagger/index.html
|
||||
|
||||
## Locally
|
||||
|
||||
Install [go](https://go.dev/).
|
||||
|
||||
### Generating the docs
|
||||
|
||||
1. Set the current working dir to `src`
|
||||
```bash
|
||||
cd src
|
||||
```
|
||||
1. Run swag to generate the docs
|
||||
* Option 1, via docker
|
||||
* Option 1, via go
|
||||
```bash
|
||||
docker run --rm -v $(pwd):/code ghcr.io/swaggo/swag:latest init --requiredByDefault
|
||||
go run github.com/swaggo/swag/cmd/swag@v1.16.6 init --requiredByDefault --outputTypes "go,json"
|
||||
```
|
||||
* Option 2, install swag and run the command line tool
|
||||
* Option 2, directly with swag
|
||||
```bash
|
||||
swag init --requiredByDefault
|
||||
swag init --requiredByDefault --outputTypes "go,json"
|
||||
```
|
||||
* Option 3, swag via docker
|
||||
```bash
|
||||
docker run --rm -v $(pwd):/code ghcr.io/swaggo/swag:latest init --requiredByDefault --outputTypes "go,json"
|
||||
```
|
||||
1. Set the current working dir to `src/docs`
|
||||
```bash
|
||||
cd docs
|
||||
```
|
||||
1. Add the signal-cli receive V1 schemas
|
||||
* Download the `signal-cli-x.y.z-json-schemas.tar.gz` schema files from https://github.com/Gara-Dorta/signal-cli/releases
|
||||
* Extract the files
|
||||
* Run the script to add the schemas
|
||||
```bash
|
||||
go run add_v1_receive_schemas.go ./path-to-signal-cli-json-schema-folder
|
||||
```
|
||||
|
||||
## Run the web server
|
||||
### Run the web server
|
||||
|
||||
Run the web server to visualize the generated docs.
|
||||
|
||||
1. Navigate to the `src` folder
|
||||
```bash
|
||||
cd src
|
||||
```
|
||||
1. Run the main script
|
||||
* Option 1, via docker, run the command at the root of the repository
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
* Option 2, install go and run the command line tool
|
||||
```bash
|
||||
cd src
|
||||
```
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
|
||||
## Navigate to the docs
|
||||
|
||||
The docs are served at: http://127.0.0.1:8080/swagger/index.html
|
||||
|
||||
When serving with docker, if you get a Network error, replace the IP for the docker internal IP in the error, e.g: http://172.18.0.2:8080/swagger/index.html
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
1. Go to http://127.0.0.1:8080/swagger/index.html
|
||||
|
||||
327
src/docs/add_v1_receive_schemas.go
Normal file
327
src/docs/add_v1_receive_schemas.go
Normal file
@ -0,0 +1,327 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
_ "github.com/bbernhard/signal-cli-rest-api/docs"
|
||||
)
|
||||
|
||||
const (
|
||||
goDocsPath = "docs.go"
|
||||
jsonDocsPath = "swagger.json"
|
||||
openMarker = "const docTemplate = `"
|
||||
closeMarker = "`\n\n// SwaggerInfo"
|
||||
schemesTemplateValue = "{{ marshal .Schemes }}"
|
||||
schemesPlaceholderToken = "__SWAG_SCHEMES_PLACEHOLDER__"
|
||||
receivePrefix = "receive."
|
||||
receivePathKey = "/v1/receive/{number}"
|
||||
receiveWrapper = "data.Message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: go run update_receive_docs.go <receiveDir>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
receiveDir := os.Args[1]
|
||||
|
||||
if err := run(receiveDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(receiveDir string) error {
|
||||
definitions := make(map[string]interface{})
|
||||
|
||||
titleByFile, err := addReceiveSchemas(definitions, receiveDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateReceiveSchemaRefs(definitions, titleByFile)
|
||||
|
||||
addEnvelopeWrapperDefinition(definitions)
|
||||
|
||||
if err := updateDocsGo(definitions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateSwaggerJSON(definitions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("updated %s\n", goDocsPath)
|
||||
fmt.Printf("updated %s\n", jsonDocsPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateDocsGo(receiveDefinitions map[string]interface{}) error {
|
||||
content, err := os.ReadFile(goDocsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", goDocsPath, err)
|
||||
}
|
||||
|
||||
template, templateStart, templateEnd, err := extractDocTemplate(string(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedTemplate, err := updateJSONDocument(toValidJson(template), receiveDefinitions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updatedTemplate = encodeForGoRawString(updatedTemplate)
|
||||
|
||||
updated := string(content[:templateStart]) + updatedTemplate + string(content[templateEnd:])
|
||||
if err := os.WriteFile(goDocsPath, []byte(updated), 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", goDocsPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSwaggerJSON(receiveDefinitions map[string]interface{}) error {
|
||||
content, err := os.ReadFile(jsonDocsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", jsonDocsPath, err)
|
||||
}
|
||||
|
||||
updated, err := updateJSONDocument(string(content), receiveDefinitions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(jsonDocsPath, []byte(updated), 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", jsonDocsPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractDocTemplate(content string) (string, int, int, error) {
|
||||
start := strings.Index(content, openMarker)
|
||||
if start == -1 {
|
||||
return "", -1, -1, fmt.Errorf("could not find docTemplate start in %s", goDocsPath)
|
||||
}
|
||||
|
||||
start += len(openMarker)
|
||||
endOffset := strings.Index(content[start:], closeMarker)
|
||||
if endOffset == -1 {
|
||||
return "", -1, -1, fmt.Errorf("could not find docTemplate end in %s", goDocsPath)
|
||||
}
|
||||
|
||||
end := start + endOffset
|
||||
return content[start:end], start, end, nil
|
||||
}
|
||||
|
||||
func toValidJson(content string) string {
|
||||
content = strings.ReplaceAll(content, "` + \"`\" + `", "`")
|
||||
content = strings.Replace(content, schemesTemplateValue, `"`+schemesPlaceholderToken+`"`, 1)
|
||||
return content
|
||||
}
|
||||
|
||||
func encodeForGoRawString(content string) string {
|
||||
content = strings.ReplaceAll(content, "`", "` + \"`\" + `")
|
||||
content = strings.Replace(content, `"`+schemesPlaceholderToken+`"`, schemesTemplateValue, 1)
|
||||
return content
|
||||
}
|
||||
|
||||
func updateJSONDocument(content string, receiveDefinitions map[string]interface{}) (string, error) {
|
||||
var document map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(content), &document); err != nil {
|
||||
return "", fmt.Errorf("parse document: %w", err)
|
||||
}
|
||||
|
||||
if err := applyReceiveSchemaUpdates(document, receiveDefinitions); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
raw, err := json.MarshalIndent(document, "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal document: %w", err)
|
||||
}
|
||||
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
func addReceiveSchemas(definitions map[string]interface{}, receiveDir string) (map[string]string, error) {
|
||||
entries, err := os.ReadDir(receiveDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", receiveDir, err)
|
||||
}
|
||||
|
||||
files := make([]string, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".schema.json") {
|
||||
continue
|
||||
}
|
||||
files = append(files, entry.Name())
|
||||
}
|
||||
sort.Strings(files)
|
||||
titleByFile := make(map[string]string, len(files))
|
||||
|
||||
for _, name := range files {
|
||||
fullPath := filepath.Join(receiveDir, name)
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read schema file %s: %w", fullPath, err)
|
||||
}
|
||||
|
||||
var schemaObj map[string]interface{}
|
||||
if err := json.Unmarshal(data, &schemaObj); err != nil {
|
||||
return nil, fmt.Errorf("parse schema file %s: %w", fullPath, err)
|
||||
}
|
||||
|
||||
title, ok := schemaObj["title"].(string)
|
||||
if !ok || strings.TrimSpace(title) == "" {
|
||||
return nil, fmt.Errorf("schema file %s is missing title", fullPath)
|
||||
}
|
||||
|
||||
titleByFile[name] = title
|
||||
definitions[receivePrefix+title] = removeSchemaKeysRecursive(schemaObj, "")
|
||||
}
|
||||
|
||||
return titleByFile, nil
|
||||
}
|
||||
|
||||
func removeSchemaKeysRecursive(value interface{}, parentKey string) interface{} {
|
||||
switch typed := value.(type) {
|
||||
case map[string]interface{}:
|
||||
updated := make(map[string]interface{}, len(typed))
|
||||
for key, item := range typed {
|
||||
if key == "$schema" || key == "$id" {
|
||||
continue
|
||||
}
|
||||
if key == "title" && parentKey != "properties" {
|
||||
continue
|
||||
}
|
||||
updated[key] = removeSchemaKeysRecursive(item, key)
|
||||
}
|
||||
return updated
|
||||
case []interface{}:
|
||||
updated := make([]interface{}, len(typed))
|
||||
for idx, item := range typed {
|
||||
updated[idx] = removeSchemaKeysRecursive(item, parentKey)
|
||||
}
|
||||
return updated
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func updateReceiveSchemaRefs(definitions map[string]interface{}, titleByFile map[string]string) {
|
||||
for key, value := range definitions {
|
||||
if !strings.HasPrefix(key, receivePrefix) {
|
||||
continue
|
||||
}
|
||||
definitions[key] = rewriteSchemaRefs(value, titleByFile)
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteSchemaRefs(value interface{}, titleByFile map[string]string) interface{} {
|
||||
switch typed := value.(type) {
|
||||
case map[string]interface{}:
|
||||
updated := make(map[string]interface{}, len(typed))
|
||||
for k, v := range typed {
|
||||
if k == "$ref" {
|
||||
if refValue, ok := v.(string); ok {
|
||||
if strings.HasSuffix(refValue, ".schema.json") {
|
||||
base := filepath.Base(refValue)
|
||||
if title, exists := titleByFile[base]; exists {
|
||||
updated[k] = "#/definitions/" + receivePrefix + title
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated[k] = rewriteSchemaRefs(v, titleByFile)
|
||||
}
|
||||
return updated
|
||||
case []interface{}:
|
||||
updated := make([]interface{}, len(typed))
|
||||
for idx, item := range typed {
|
||||
updated[idx] = rewriteSchemaRefs(item, titleByFile)
|
||||
}
|
||||
return updated
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func addEnvelopeWrapperDefinition(definitions map[string]interface{}) {
|
||||
definitions[receiveWrapper] = map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"account": map[string]interface{}{"type": "string"},
|
||||
"envelope": map[string]interface{}{"$ref": "#/definitions/receive.MessageEnvelope"},
|
||||
},
|
||||
"required": []interface{}{"account", "envelope"},
|
||||
}
|
||||
}
|
||||
|
||||
func applyReceiveSchemaUpdates(document map[string]interface{}, receiveDefinitions map[string]interface{}) error {
|
||||
definitions, err := getObject(document, "definitions")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key := range definitions {
|
||||
if strings.HasPrefix(key, receivePrefix) || key == receiveWrapper {
|
||||
delete(definitions, key)
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range receiveDefinitions {
|
||||
definitions[key] = value
|
||||
}
|
||||
|
||||
paths, err := getObject(document, "paths")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
receivePath, err := getObject(paths, receivePathKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
receiveGet, err := getObject(receivePath, "get")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responses, err := getObject(receiveGet, "responses")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response200, err := getObject(responses, "200")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response200["schema"] = map[string]interface{}{
|
||||
"$ref": "#/definitions/" + receiveWrapper,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getObject(parent map[string]interface{}, key string) (map[string]interface{}, error) {
|
||||
value, ok := parent[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing key %q", key)
|
||||
}
|
||||
|
||||
obj, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key %q is not an object", key)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
7530
src/docs/docs.go
7530
src/docs/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user