Merge 82f71512129b1d32603a6b5c695f5287e652b864 into db63fd15e0bc2e2de4cff0b1969b12c23508a8d7

This commit is contained in:
Gara Dorta 2026-05-10 23:23:47 +01:00 committed by GitHub
commit 68ecd0df66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 3147 additions and 2627 deletions

View File

@ -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.mod /tmp/signal-cli-rest-api-src/
COPY src/go.sum /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/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 # 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 build -o signal-cli-rest-api main.go
RUN cd /tmp/signal-cli-rest-api-src && go test ./client -v && go test ./utils -v RUN cd /tmp/signal-cli-rest-api-src && go test ./client -v && go test ./utils -v

View File

@ -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. 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` 1. Set the current working dir to `src`
```bash ```bash
cd src cd src
``` ```
1. Run swag to generate the docs 1. Run swag to generate the docs
* Option 1, via docker * Option 1, via go
```bash ```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 ```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. Run the web server to visualize the generated docs.
1. Run the main script 1. Navigate to the `src` folder
* 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 ```bash
cd src cd src
``` ```
1. Run the main script
```bash ```bash
go run main.go go run main.go
``` ```
1. Go to http://127.0.0.1:8080/swagger/index.html
## 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

View File

@ -0,0 +1,409 @@
//go:build ignore
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
_ "github.com/bbernhard/signal-cli-rest-api/docs"
)
const (
goDocsPath = "docs.go"
jsonDocsPath = "swagger.json"
openMarker = "const docTemplate = `"
closeMarker = "`\n\n// SwaggerInfo"
definitionsKey = `"definitions": {`
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
}
if err := updateReceiveSchemaRefs(definitions, titleByFile); err != nil {
return err
}
addEnvelopeWrapperDefinition(definitions)
managedDefinitions, err := renderManagedDefinitions(definitions)
if err != nil {
return err
}
if err := updateDocsGo(managedDefinitions); err != nil {
return err
}
if err := updateSwaggerJSON(managedDefinitions); err != nil {
return err
}
fmt.Printf("updated %s\n", goDocsPath)
fmt.Printf("updated %s\n", jsonDocsPath)
return nil
}
func updateDocsGo(managedDefinitions string) error {
content, err := os.ReadFile(goDocsPath)
if err != nil {
return fmt.Errorf("read %s: %w", goDocsPath, err)
}
template, err := extractDocTemplate(string(content))
if err != nil {
return err
}
updatedTemplate, err := applyReceiveSchemaUpdates(template, managedDefinitions)
if err != nil {
return err
}
updated := strings.Replace(string(content), template, updatedTemplate, 1)
if err := os.WriteFile(goDocsPath, []byte(updated), 0644); err != nil {
return fmt.Errorf("write %s: %w", goDocsPath, err)
}
return nil
}
func updateSwaggerJSON(managedDefinitions string) error {
content, err := os.ReadFile(jsonDocsPath)
if err != nil {
return fmt.Errorf("read %s: %w", jsonDocsPath, err)
}
updated, err := applyReceiveSchemaUpdates(string(content), managedDefinitions)
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 applyReceiveSchemaUpdates(content string, managedDefinitions string) (string, error) {
updated, err := appendDefinitionsEntries(content, managedDefinitions)
if err != nil {
return "", err
}
updated, err = replaceReceiveResponseSchema(updated)
if err != nil {
return "", err
}
return updated, nil
}
func extractDocTemplate(content string) (string, error) {
start := strings.Index(content, openMarker)
if start == -1 {
return "", fmt.Errorf("could not find docTemplate start in %s", goDocsPath)
}
start += len(openMarker)
endOffset := strings.Index(content[start:], closeMarker)
if endOffset == -1 {
return "", fmt.Errorf("could not find docTemplate end in %s", goDocsPath)
}
return content[start : start+endOffset], nil
}
func definitionsBounds(template string) (int, int, error) {
definitionsIndex := strings.Index(template, definitionsKey)
if definitionsIndex == -1 {
return -1, -1, fmt.Errorf("could not find definitions block in docTemplate")
}
braceIndex := definitionsIndex + strings.Index(definitionsKey, "{")
closingBraceIndex, err := findMatchingBrace(template, braceIndex)
if err != nil {
return -1, -1, err
}
return braceIndex, closingBraceIndex, nil
}
func appendDefinitionsEntries(template string, entries string) (string, error) {
braceIndex, closingBraceIndex, err := definitionsBounds(template)
if err != nil {
return "", err
}
definitionsBlock := template[braceIndex : closingBraceIndex+1]
if strings.Contains(definitionsBlock, `"`+receiveWrapper+`"`) || strings.Contains(definitionsBlock, `"`+receivePrefix) {
return "", fmt.Errorf("definitions already contain receive entries; run swag init first")
}
inner := strings.TrimSpace(template[braceIndex+1 : closingBraceIndex])
if inner == "" {
return template[:braceIndex+1] + "\n" + entries + "\n" + template[closingBraceIndex:], nil
}
closingLine := "\n }"
insertIndex := strings.LastIndex(template[:closingBraceIndex+1], closingLine)
if insertIndex == -1 {
return "", fmt.Errorf("could not determine definitions closing line")
}
return template[:insertIndex] + ",\n" + entries + template[insertIndex:], 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) error {
for key, value := range definitions {
if !strings.HasPrefix(key, receivePrefix) {
continue
}
definitions[key] = rewriteSchemaRefs(value, titleByFile)
}
return nil
}
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 renderManagedDefinitions(definitions map[string]interface{}) (string, error) {
keys := make([]string, 0, len(definitions))
for key := range definitions {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
raw, err := json.MarshalIndent(definitions[key], "", " ")
if err != nil {
return "", fmt.Errorf("marshal definition %s: %w", key, err)
}
lines := strings.Split(string(raw), "\n")
for idx := range lines {
lines[idx] = " " + lines[idx]
}
entry := " " + strconv.Quote(key) + ": " + strings.TrimPrefix(lines[0], " ")
if len(lines) > 1 {
entry += "\n" + strings.Join(lines[1:], "\n")
}
parts = append(parts, entry)
}
return strings.Join(parts, ",\n"), nil
}
func replaceReceiveResponseSchema(template string) (string, error) {
pathIndex := strings.Index(template, receivePathKey)
if pathIndex == -1 {
return "", fmt.Errorf("could not find receive path block; run swag init first")
}
braceOffset := strings.Index(template[pathIndex+len(receivePathKey):], "{")
if braceOffset == -1 {
return "", fmt.Errorf("could not find opening brace for receive path block")
}
pathOpenBrace := pathIndex + len(receivePathKey) + braceOffset
pathCloseBrace, err := findMatchingBrace(template, pathOpenBrace)
if err != nil {
return "", err
}
pathBlock := template[pathOpenBrace : pathCloseBrace+1]
oldSchema := `"schema": {
"type": "array",
"items": {
"type": "string"
}
}`
newSchema := `"schema": {
"$ref": "#/definitions/data.Message"
}`
updatedPathBlock := strings.Replace(pathBlock, oldSchema, newSchema, 1)
if updatedPathBlock == pathBlock {
return "", fmt.Errorf("could not replace /v1/receive schema; ensure generated docs are freshly generated by swag")
}
return template[:pathOpenBrace] + updatedPathBlock + template[pathCloseBrace+1:], nil
}
func findMatchingBrace(input string, openBraceIndex int) (int, error) {
depth := 0
inString := false
escaped := false
for index := openBraceIndex; index < len(input); index++ {
char := input[index]
if inString {
if escaped {
escaped = false
continue
}
if char == '\\' {
escaped = true
continue
}
if char == '"' {
inString = false
}
continue
}
if char == '"' {
inString = true
continue
}
switch char {
case '{':
depth++
case '}':
depth--
if depth == 0 {
return index, nil
}
}
}
return -1, fmt.Errorf("could not find matching brace")
}

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