refactor: extract all generators into independent TOOLS/ modules
Each generator is now a standalone Go module with its own go.mod: TOOLS/ ├── yaml-generator/ ← service_spec_gen (API → YAML) ├── resource-generator/ ← gen_v2 (YAML → Go resources) ├── docs-generator/ ← docs_template_gen_v2 (YAML → Markdown) ├── ops-generator/ ← ops_docs_gen (ops documentation) └── bin/ ← pre-built binaries All scripts updated to use TOOLS/bin/* binaries. Removed old universal_rebuild/tools/ and universal_rebuild/bin/.
This commit is contained in:
@@ -88,3 +88,4 @@ universal_rebuild/service_params_gen
|
|||||||
# ── Build artifacts ──
|
# ── Build artifacts ──
|
||||||
terraform-provider-mycloud
|
terraform-provider-mycloud
|
||||||
artifacts/api-meta/*/errors.log
|
artifacts/api-meta/*/errors.log
|
||||||
|
TOOLS/bin/
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module docs-generator
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module ops-generator
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module resource-generator
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
+1
-1
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/gen_v2/types"
|
"resource-generator/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToCamel конвертирует snake_case → CamelCase.
|
// ToCamel конвертирует snake_case → CamelCase.
|
||||||
+2
-2
@@ -11,8 +11,8 @@ import (
|
|||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/gen_v2/params"
|
"resource-generator/params"
|
||||||
"terraform-provider-nubes/tools/gen_v2/types"
|
"resource-generator/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadSpecs загружает все YAML-спеки из директории и строит GenResource/GenSubresource/GenAction.
|
// LoadSpecs загружает все YAML-спеки из директории и строит GenResource/GenSubresource/GenAction.
|
||||||
@@ -20,8 +20,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/gen_v2/loader"
|
"resource-generator/loader"
|
||||||
"terraform-provider-nubes/tools/gen_v2/writers"
|
"resource-generator/writers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/gen_v2/types"
|
"resource-generator/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Merge объединяет несколько наборов параметров, убирая дубликаты по Code.
|
// Merge объединяет несколько наборов параметров, убирая дубликаты по Code.
|
||||||
+3
-3
@@ -8,9 +8,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/gen_v2/helpers"
|
"resource-generator/helpers"
|
||||||
"terraform-provider-nubes/tools/gen_v2/templates"
|
"resource-generator/templates"
|
||||||
"terraform-provider-nubes/tools/gen_v2/types"
|
"resource-generator/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteInstanceResource генерирует nubes_{service} (CRUD инстанса).
|
// WriteInstanceResource генерирует nubes_{service} (CRUD инстанса).
|
||||||
+2
-2
@@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/normalize"
|
"yaml-generator/normalize"
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/types"
|
"yaml-generator/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client — HTTP-клиент для Nubes API.
|
// Client — HTTP-клиент для Nubes API.
|
||||||
+1
-1
@@ -9,7 +9,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/normalize"
|
"yaml-generator/normalize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config — конфигурация генератора.
|
// Config — конфигурация генератора.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module yaml-generator
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/client"
|
"yaml-generator/client"
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/config"
|
"yaml-generator/config"
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/normalize"
|
"yaml-generator/normalize"
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/spec"
|
"yaml-generator/spec"
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/types"
|
"yaml-generator/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"terraform-provider-nubes/tools/service_spec_gen/types"
|
"yaml-generator/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultOutputParams возвращает стандартный набор выходных параметров.
|
// DefaultOutputParams возвращает стандартный набор выходных параметров.
|
||||||
@@ -138,9 +138,9 @@ fi
|
|||||||
rm -f "$FAILURES_FILE"
|
rm -f "$FAILURES_FILE"
|
||||||
|
|
||||||
# Build service_spec_gen binary if missing.
|
# Build service_spec_gen binary if missing.
|
||||||
if [[ ! -x "${PROVIDER_DIR}/bin/service_spec_gen" ]]; then
|
if [[ ! -x "${ROOT_DIR}/TOOLS/bin/yaml-generator" ]]; then
|
||||||
echo "Building service_spec_gen..."
|
echo "Building service_spec_gen..."
|
||||||
(cd "$PROVIDER_DIR" && go build -o bin/service_spec_gen ./tools/service_spec_gen/)
|
# Binary pre-built in TOOLS/bin/yaml-generator
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Проверка что список сервисов не пустой
|
# Проверка что список сервисов не пустой
|
||||||
@@ -235,7 +235,7 @@ PY
|
|||||||
NUBES_SERVICE_NAME="$svc_name" \
|
NUBES_SERVICE_NAME="$svc_name" \
|
||||||
NUBES_API_ENDPOINT="$API_ENDPOINT" \
|
NUBES_API_ENDPOINT="$API_ENDPOINT" \
|
||||||
NUBES_OUTPUT_DIR="$YAML_OUTPUT_DIR" \
|
NUBES_OUTPUT_DIR="$YAML_OUTPUT_DIR" \
|
||||||
"${PROVIDER_DIR}/bin/service_spec_gen"
|
"${ROOT_DIR}/TOOLS/bin/yaml-generator"
|
||||||
) && success=1 || success=0
|
) && success=1 || success=0
|
||||||
|
|
||||||
if [[ "$success" -eq 1 ]]; then
|
if [[ "$success" -eq 1 ]]; then
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ rm -f "${PROVIDER_DIR}/internal/resources_gen/registry.go"
|
|||||||
|
|
||||||
# Step 1: generate Go resources from unified YAML.
|
# Step 1: generate Go resources from unified YAML.
|
||||||
echo "Generating Go resources from unified YAML..."
|
echo "Generating Go resources from unified YAML..."
|
||||||
go run ./tools/gen_v2
|
${ROOT_DIR}/TOOLS/bin/resource-generator
|
||||||
|
|
||||||
# Step 2: generate documentation from unified YAML (new template).
|
# Step 2: generate documentation from unified YAML (new template).
|
||||||
echo "Generating documentation using new template generator..."
|
echo "Generating documentation using new template generator..."
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ SERVICES_LIST_PATH="${ROOT_DIR}/devops/config/services_list.txt"
|
|||||||
|
|
||||||
cd "$PROVIDER_DIR"
|
cd "$PROVIDER_DIR"
|
||||||
|
|
||||||
go run ./tools/docs_template_gen \
|
${ROOT_DIR}/TOOLS/bin/docs-generator \
|
||||||
-resources "$RESOURCES_YAML_DIR" \
|
-resources "$RESOURCES_YAML_DIR" \
|
||||||
-docs "$DOCS_DIR" \
|
-docs "$DOCS_DIR" \
|
||||||
-services "$SERVICES_LIST_PATH" \
|
-services "$SERVICES_LIST_PATH" \
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ SERVICES_LIST_PATH="${ROOT_DIR}/devops/config/services_list.txt"
|
|||||||
|
|
||||||
cd "$PROVIDER_DIR"
|
cd "$PROVIDER_DIR"
|
||||||
|
|
||||||
go run ./tools/docs_template_gen_v2 \
|
${ROOT_DIR}/TOOLS/bin/docs-generator \
|
||||||
-resources "$RESOURCES_YAML_DIR" \
|
-resources "$RESOURCES_YAML_DIR" \
|
||||||
-docs "$DOCS_DIR" \
|
-docs "$DOCS_DIR" \
|
||||||
-services "$SERVICES_LIST_PATH" \
|
-services "$SERVICES_LIST_PATH" \
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ trap 'rm -rf "$TMP_GEN_DIR"' EXIT
|
|||||||
|
|
||||||
NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \
|
NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \
|
||||||
NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \
|
NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \
|
||||||
go run ./tools/gen_v2
|
${ROOT_DIR}/TOOLS/bin/resource-generator
|
||||||
|
|
||||||
mkdir -p "$GO_OUTPUT_DIR"
|
mkdir -p "$GO_OUTPUT_DIR"
|
||||||
rm -rf "${GO_OUTPUT_DIR}"/*
|
rm -rf "${GO_OUTPUT_DIR}"/*
|
||||||
@@ -99,7 +99,7 @@ if [[ "$DOCS_API_ENDPOINT" != *"/index.cfm"* ]] && [[ "$DOCS_API_ENDPOINT" != *"
|
|||||||
DOCS_API_ENDPOINT="${DOCS_API_ENDPOINT%/}/index.cfm"
|
DOCS_API_ENDPOINT="${DOCS_API_ENDPOINT%/}/index.cfm"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
go run ./tools/docs_template_gen_v2 \
|
${ROOT_DIR}/TOOLS/bin/docs-generator \
|
||||||
-resources "$RESOURCES_YAML_DIR" \
|
-resources "$RESOURCES_YAML_DIR" \
|
||||||
-docs "$DOCS_DIR" \
|
-docs "$DOCS_DIR" \
|
||||||
-services "$SERVICES_LIST_PATH" \
|
-services "$SERVICES_LIST_PATH" \
|
||||||
|
|||||||
@@ -1,746 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ParamSpec struct {
|
|
||||||
ID int `yaml:"id"`
|
|
||||||
Code string `yaml:"code"`
|
|
||||||
DataType string `yaml:"data_type"`
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Required bool `yaml:"required"`
|
|
||||||
Default interface{} `yaml:"default"`
|
|
||||||
Descr string `yaml:"descr"`
|
|
||||||
Man string `yaml:"man"`
|
|
||||||
RefSvcID *int `yaml:"ref_svc_id"`
|
|
||||||
Func string `yaml:"func"`
|
|
||||||
MinValue *int `yaml:"minvalue"`
|
|
||||||
MaxValue *int `yaml:"maxvalue"`
|
|
||||||
Regex string `yaml:"regex"`
|
|
||||||
ValueList []string `yaml:"value_list"`
|
|
||||||
Unique string `yaml:"unique_scope"`
|
|
||||||
MaxLength *int `yaml:"maxlength"`
|
|
||||||
MinLength *int `yaml:"minlength"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OperationSpec struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
ID int `yaml:"id"`
|
|
||||||
Kind string `yaml:"kind"`
|
|
||||||
Action string `yaml:"action"`
|
|
||||||
Subresource string `yaml:"subresource"`
|
|
||||||
Man string `yaml:"man"`
|
|
||||||
Params []ParamSpec `yaml:"params"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputParam struct {
|
|
||||||
Code string `yaml:"code"`
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Sensitive bool `yaml:"sensitive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceSpec struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
ServiceID int `yaml:"service_id"`
|
|
||||||
ServiceDisplayName string `yaml:"service_display_name"`
|
|
||||||
ServiceMan string `yaml:"service_man"`
|
|
||||||
Lifecycle struct {
|
|
||||||
DeleteModeDefault string `yaml:"delete_mode_default"`
|
|
||||||
ResumeIfExistsDefault bool `yaml:"resume_if_exists_default"`
|
|
||||||
} `yaml:"lifecycle"`
|
|
||||||
Outputs struct {
|
|
||||||
Params []OutputParam `yaml:"params"`
|
|
||||||
} `yaml:"outputs"`
|
|
||||||
Operations []OperationSpec `yaml:"operations"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceMeta struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
resourcesDirFlag := flag.String("resources", "", "Path to resources_yaml")
|
|
||||||
docsDirFlag := flag.String("docs", "", "Path to docs/30_registry/resources")
|
|
||||||
servicesListFlag := flag.String("services", "", "Path to devops/config/services_list.txt")
|
|
||||||
excludeFlag := flag.String("exclude", "clickhouse", "Comma-separated resource names to skip")
|
|
||||||
versionFlag := flag.String("version", "", "Provider version for example block")
|
|
||||||
apiEndpointFlag := flag.String("api-endpoint", "https://deck-api.ngcloud.ru/api/v1/index.cfm", "API endpoint for example block")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
root := detectRoot()
|
|
||||||
resourcesDir := pickPath(*resourcesDirFlag, filepath.Join(root, "universal_rebuild", "resources_yaml"))
|
|
||||||
docsDir := pickPath(*docsDirFlag, filepath.Join(root, "docs", "30_registry", "resources"))
|
|
||||||
servicesList := pickPath(*servicesListFlag, filepath.Join(root, "devops", "config", "services_list.txt"))
|
|
||||||
excludeSet := toSet(*excludeFlag)
|
|
||||||
version := *versionFlag
|
|
||||||
if version == "" {
|
|
||||||
version = detectVersion(filepath.Join(root, "universal_rebuild", "main.go"))
|
|
||||||
}
|
|
||||||
apiEndpoint := *apiEndpointFlag
|
|
||||||
|
|
||||||
servicesOrder := loadServicesList(servicesList)
|
|
||||||
specs := loadSpecs(resourcesDir, servicesOrder)
|
|
||||||
if len(specs) == 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, "No specs found in %s\n", resourcesDir)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(docsDir, 0o755); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, spec := range specs {
|
|
||||||
if excludeSet[spec.Name] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
writeResourceDocs(docsDir, spec, version, apiEndpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectRoot() string {
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if filepath.Base(cwd) == "universal_rebuild" {
|
|
||||||
return filepath.Dir(cwd)
|
|
||||||
}
|
|
||||||
return cwd
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickPath(value, fallback string) string {
|
|
||||||
if value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func toSet(csv string) map[string]bool {
|
|
||||||
out := map[string]bool{}
|
|
||||||
for _, item := range strings.Split(csv, ",") {
|
|
||||||
trimmed := strings.TrimSpace(item)
|
|
||||||
if trimmed == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out[trimmed] = true
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectVersion(mainPath string) string {
|
|
||||||
b, err := os.ReadFile(mainPath)
|
|
||||||
if err != nil {
|
|
||||||
return "2.x"
|
|
||||||
}
|
|
||||||
re := regexp.MustCompile(`version string\s*=\s*"([0-9.]+)"`)
|
|
||||||
match := re.FindStringSubmatch(string(b))
|
|
||||||
if len(match) > 1 {
|
|
||||||
return match[1]
|
|
||||||
}
|
|
||||||
return "2.x"
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadServicesList(path string) []ServiceMeta {
|
|
||||||
b, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
lines := strings.Split(string(b), "\n")
|
|
||||||
out := []ServiceMeta{}
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
head := strings.SplitN(line, "#", 2)[0]
|
|
||||||
parts := strings.Fields(head)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
id, err := strconv.Atoi(parts[0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, ServiceMeta{ID: id, Name: parts[1]})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadSpecs(dir string, ordered []ServiceMeta) []ServiceSpec {
|
|
||||||
specsByID := map[int]ServiceSpec{}
|
|
||||||
walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var spec ServiceSpec
|
|
||||||
if err := yaml.Unmarshal(b, &spec); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
specsByID[spec.ServiceID] = spec
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if walkErr != nil {
|
|
||||||
panic(walkErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ordered) == 0 {
|
|
||||||
specs := make([]ServiceSpec, 0, len(specsByID))
|
|
||||||
for _, spec := range specsByID {
|
|
||||||
specs = append(specs, spec)
|
|
||||||
}
|
|
||||||
sort.Slice(specs, func(i, j int) bool { return specs[i].ServiceID < specs[j].ServiceID })
|
|
||||||
return specs
|
|
||||||
}
|
|
||||||
|
|
||||||
specs := []ServiceSpec{}
|
|
||||||
for _, meta := range ordered {
|
|
||||||
spec, ok := specsByID[meta.ID]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
specs = append(specs, spec)
|
|
||||||
}
|
|
||||||
return specs
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeResourceDocs(docsDir string, spec ServiceSpec, version string, apiEndpoint string) {
|
|
||||||
base := spec.Name
|
|
||||||
nav := fmt.Sprintf("[Manual](%s.md) | [Create params](%s_params_create.md) | [Modify params](%s_params_modify.md) | [Output params](%s_outputs.md) | [Operations](%s_ops.md) | [Example](%s_example.md)", base, base, base, base, base, base)
|
|
||||||
|
|
||||||
writeFile(filepath.Join(docsDir, base+".md"), buildManualPage(spec, nav))
|
|
||||||
writeFile(filepath.Join(docsDir, base+"_example.md"), buildExamplePage(spec, nav, version, apiEndpoint))
|
|
||||||
writeFile(filepath.Join(docsDir, base+"_params_create.md"), buildCreateParamsPage(spec, nav))
|
|
||||||
writeFile(filepath.Join(docsDir, base+"_params_modify.md"), buildModifyParamsPage(spec, nav))
|
|
||||||
writeFile(filepath.Join(docsDir, base+"_outputs.md"), buildOutputsPage(spec, nav))
|
|
||||||
writeFile(filepath.Join(docsDir, base+"_ops.md"), buildOpsPage(spec, nav, docsDir))
|
|
||||||
writeFile(filepath.Join(docsDir, base+"_params.md"), buildParamsLandingPage(spec, nav))
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeFile(path, content string) {
|
|
||||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHeader(spec ServiceSpec, nav string) string {
|
|
||||||
name := spec.ServiceDisplayName
|
|
||||||
if name == "" {
|
|
||||||
name = spec.Name
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("# Resource nubes_%s\n\nService ID: `%d`\n\nService Name: %s\n\n%s\n\n", spec.Name, spec.ServiceID, name, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildManualPage(spec ServiceSpec, nav string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(buildHeader(spec, nav))
|
|
||||||
b.WriteString("## MAN\n\n")
|
|
||||||
man := strings.TrimSpace(spec.ServiceMan)
|
|
||||||
if man == "" {
|
|
||||||
man = "Manual not available."
|
|
||||||
}
|
|
||||||
b.WriteString("<div class=\"man-content\">\n")
|
|
||||||
b.WriteString(man)
|
|
||||||
b.WriteString("\n</div>\n")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildExamplePage(spec ServiceSpec, nav, version string, apiEndpoint string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(buildHeader(spec, nav))
|
|
||||||
b.WriteString(fmt.Sprintf("## Copy-ready manifest (`%s_main.tf`)\n\n", spec.Name))
|
|
||||||
b.WriteString("<div class=\"copy-ready-code\"><pre><code>")
|
|
||||||
b.WriteString(exampleBlock(spec, version, apiEndpoint))
|
|
||||||
b.WriteString("</code></pre></div>\n\n")
|
|
||||||
b.WriteString("## Outputs usage\n\n")
|
|
||||||
b.WriteString("<div class=\"copy-ready-code\"><pre><code>")
|
|
||||||
b.WriteString("# Выходные параметры можно использовать как\n")
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.state_params[\"имя_ключа\"]\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.state_out[\"имя_ключа\"]\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.state_params_flat[\"имя_ключа\"]\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.state_out_flat[\"имя_ключа\"]\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_secrets[\"имя_ключа\"]\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_url\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_user_path\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("nubes_%s.baza.vault_fields\n", spec.Name))
|
|
||||||
b.WriteString("</code></pre></div>\n")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleBlock(spec ServiceSpec, version string, apiEndpoint string) string {
|
|
||||||
createParams := findParams(spec.Operations, "create")
|
|
||||||
requiredParams, defaultParams := splitParams(createParams)
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("terraform {\n")
|
|
||||||
b.WriteString(" required_providers {\n")
|
|
||||||
b.WriteString(" nubes = {\n")
|
|
||||||
b.WriteString(" source = \"terra.k8c.ru/nubes/nubes\"\n")
|
|
||||||
b.WriteString(fmt.Sprintf(" version = \"%s\"\n", version))
|
|
||||||
b.WriteString(" }\n")
|
|
||||||
b.WriteString(" }\n")
|
|
||||||
b.WriteString("}\n\n")
|
|
||||||
b.WriteString("# Токен доступа api_token к Nubes API — замените на реальный.\n")
|
|
||||||
b.WriteString("provider \"nubes\" {\n")
|
|
||||||
b.WriteString(" api_token = \"token_***\"\n")
|
|
||||||
b.WriteString(fmt.Sprintf(" api_endpoint = \"%s\"\n", apiEndpoint))
|
|
||||||
b.WriteString("}\n")
|
|
||||||
b.WriteString(fmt.Sprintf("resource \"nubes_%s\" \"baza\" {\n", spec.Name))
|
|
||||||
|
|
||||||
if len(requiredParams) > 0 {
|
|
||||||
b.WriteString(" # TODO - обязательный параметр, задаваемый пользователем\n")
|
|
||||||
}
|
|
||||||
for _, p := range requiredParams {
|
|
||||||
b.WriteString(formatParamLine(p, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(defaultParams) > 0 {
|
|
||||||
b.WriteString("\n # Параметры, имеющие значение по умолчанию, если не меняете - эти параметры не обязательно прописывать в манифесте\n")
|
|
||||||
for _, p := range defaultParams {
|
|
||||||
b.WriteString(formatParamLine(p, " "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("}\n")
|
|
||||||
return escapeCode(b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildCreateParamsPage(spec ServiceSpec, nav string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(buildHeader(spec, nav))
|
|
||||||
b.WriteString("## Create params\n\n")
|
|
||||||
createParams := findParams(spec.Operations, "create")
|
|
||||||
requiredParams, defaultParams := splitParams(createParams)
|
|
||||||
|
|
||||||
b.WriteString("**Обязательные параметры (вводимые пользователем)**\n\n")
|
|
||||||
b.WriteString(renderParamTable(requiredParams, true, true))
|
|
||||||
|
|
||||||
b.WriteString("\n**Параметры, имеющие значение по умолчанию, если не меняете - эти параметры не обязательно прописывать в манифесте**\n")
|
|
||||||
b.WriteString(renderParamTable(defaultParams, false, false))
|
|
||||||
|
|
||||||
lifecycle := spec.Lifecycle
|
|
||||||
if lifecycle.DeleteModeDefault != "" || lifecycle.ResumeIfExistsDefault {
|
|
||||||
b.WriteString("\n<div class=\"lifecycle-note\">\n")
|
|
||||||
b.WriteString("<strong>Параметры жизненного цикла (Lifecycle defaults)</strong><br/>\n")
|
|
||||||
b.WriteString(fmt.Sprintf("По умолчанию: `delete_mode_default = %s`, `resume_if_exists_default = %v`.<br/>\n", lifecycle.DeleteModeDefault, lifecycle.ResumeIfExistsDefault))
|
|
||||||
b.WriteString("Если для сервиса доступны операции `suspend` и `resume`, то `delete` выполняет приостановку (без удаления данных),<br/>\n")
|
|
||||||
b.WriteString("а `apply` при необходимости возобновляет работу существующего ресурса.<br/>\n")
|
|
||||||
b.WriteString("Менять эти значения стоит только в продвинутых сценариях — через параметры `delete_mode` и `resume_if_exists` в манифесте.\n")
|
|
||||||
b.WriteString("</div>\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildModifyParamsPage(spec ServiceSpec, nav string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(buildHeader(spec, nav))
|
|
||||||
b.WriteString("## Modify params\n\n")
|
|
||||||
modifyParams := findParams(spec.Operations, "modify")
|
|
||||||
b.WriteString(renderModifyTable(modifyParams))
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildOutputsPage(spec ServiceSpec, nav string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(buildHeader(spec, nav))
|
|
||||||
b.WriteString("## Output params\n\n")
|
|
||||||
if len(spec.Outputs.Params) == 0 {
|
|
||||||
b.WriteString("None.\n")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
for _, p := range spec.Outputs.Params {
|
|
||||||
b.WriteString(fmt.Sprintf("- `%s` (%s)\n", p.Code, p.Type))
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildOpsPage(spec ServiceSpec, nav, docsDir string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(buildHeader(spec, nav))
|
|
||||||
b.WriteString("## Operations\n\n")
|
|
||||||
for _, op := range spec.Operations {
|
|
||||||
name := op.Name
|
|
||||||
if name == "" {
|
|
||||||
name = op.Action
|
|
||||||
}
|
|
||||||
descr := strings.TrimSpace(stripHTML(op.Man))
|
|
||||||
if descr != "" {
|
|
||||||
descr = ": " + descr
|
|
||||||
}
|
|
||||||
switch op.Action {
|
|
||||||
case "create":
|
|
||||||
b.WriteString(fmt.Sprintf("- `%s` — Создание кластера. Параметры: [%s params](%s_params_create.md)\n", name, "Create", spec.Name))
|
|
||||||
continue
|
|
||||||
case "modify":
|
|
||||||
b.WriteString(fmt.Sprintf("- `%s` — Изменение кластера. Параметры: [%s params](%s_params_modify.md)\n", name, "Modify", spec.Name))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if op.Kind == "subresource" && op.Subresource != "" {
|
|
||||||
subFile := fmt.Sprintf("%s_%s.md", spec.Name, slug(op.Subresource))
|
|
||||||
if fileExists(filepath.Join(docsDir, subFile)) {
|
|
||||||
b.WriteString(fmt.Sprintf("- `%s` — %s. См. [nubes_%s_%s](%s)\n", name, strings.TrimPrefix(descr, ": "), spec.Name, slug(op.Subresource), subFile))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if descr != "" {
|
|
||||||
b.WriteString(fmt.Sprintf("- `%s` — %s\n", name, strings.TrimPrefix(descr, ": ")))
|
|
||||||
} else {
|
|
||||||
b.WriteString(fmt.Sprintf("- `%s`\n", name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildParamsLandingPage(spec ServiceSpec, nav string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString(buildHeader(spec, nav))
|
|
||||||
b.WriteString("## Parameters\n\n")
|
|
||||||
b.WriteString(fmt.Sprintf("- [Create params](%s_params_create.md)\n", spec.Name))
|
|
||||||
b.WriteString(fmt.Sprintf("- [Modify params](%s_params_modify.md)\n", spec.Name))
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func findParams(ops []OperationSpec, action string) []ParamSpec {
|
|
||||||
for _, op := range ops {
|
|
||||||
if op.Kind == "instance" && op.Action == action {
|
|
||||||
return op.Params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitParams(params []ParamSpec) ([]ParamSpec, []ParamSpec) {
|
|
||||||
required := []ParamSpec{}
|
|
||||||
defaults := []ParamSpec{}
|
|
||||||
for _, p := range params {
|
|
||||||
if hasDefault(p.Default) {
|
|
||||||
defaults = append(defaults, p)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
required = append(required, p)
|
|
||||||
}
|
|
||||||
return required, defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasDefault(value interface{}) bool {
|
|
||||||
if value == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if s, ok := value.(string); ok {
|
|
||||||
return strings.TrimSpace(s) != ""
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderParamTable(params []ParamSpec, requiredTable, noDefault bool) string {
|
|
||||||
if len(params) == 0 {
|
|
||||||
return "None.\n"
|
|
||||||
}
|
|
||||||
var b strings.Builder
|
|
||||||
tableClass := "resource-table resource-table-compact"
|
|
||||||
if requiredTable {
|
|
||||||
tableClass += " resource-table-required"
|
|
||||||
}
|
|
||||||
b.WriteString(fmt.Sprintf("<table class=\"%s\">\n", tableClass))
|
|
||||||
if noDefault {
|
|
||||||
b.WriteString("<thead><tr><th>ID</th><th>Code</th><th>Type</th><th>Description</th><th>Constraints</th></tr></thead>\n<tbody>\n")
|
|
||||||
} else {
|
|
||||||
b.WriteString("<thead><tr><th>ID</th><th>Code</th><th>Type</th><th>Default</th><th>Description</th><th>Constraints</th></tr></thead>\n<tbody>\n")
|
|
||||||
}
|
|
||||||
for _, p := range params {
|
|
||||||
b.WriteString("<tr>")
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", escapeText(formatID(p.ID))))
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", formatCode(p.Code)))
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", formatTypeCell(p)))
|
|
||||||
if !noDefault {
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", defaultCell(p.Default)))
|
|
||||||
}
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", escapeText(pickText(p))))
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", escapeText(collectConstraints(p))))
|
|
||||||
b.WriteString("</tr>\n")
|
|
||||||
}
|
|
||||||
b.WriteString("</tbody></table>\n")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderModifyTable(params []ParamSpec) string {
|
|
||||||
if len(params) == 0 {
|
|
||||||
return "None.\n"
|
|
||||||
}
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("<table class=\"resource-table resource-table-compact\">\n")
|
|
||||||
b.WriteString("<thead><tr><th>ID</th><th>Code</th><th>Type</th><th>Description</th><th>Constraints</th></tr></thead>\n<tbody>\n")
|
|
||||||
for _, p := range params {
|
|
||||||
b.WriteString("<tr>")
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", escapeText(formatID(p.ID))))
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", formatCode(p.Code)))
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", formatTypeCell(p)))
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", escapeText(pickText(p))))
|
|
||||||
b.WriteString(fmt.Sprintf("<td>%s</td>", escapeText(collectConstraints(p))))
|
|
||||||
b.WriteString("</tr>\n")
|
|
||||||
}
|
|
||||||
b.WriteString("</tbody></table>\n")
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatParamLine(p ParamSpec, indent string) string {
|
|
||||||
value := sampleValue(p)
|
|
||||||
comment := strings.TrimSpace(stripHTML(p.Man))
|
|
||||||
if comment == "" {
|
|
||||||
comment = strings.TrimSpace(stripHTML(p.Descr))
|
|
||||||
}
|
|
||||||
if comment != "" {
|
|
||||||
return fmt.Sprintf("%s%s = %s # %s\n", indent, p.Code, value, comment)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s%s = %s\n", indent, p.Code, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sampleValue(p ParamSpec) string {
|
|
||||||
if hasDefault(p.Default) {
|
|
||||||
return formatLiteral(p.Default)
|
|
||||||
}
|
|
||||||
if len(p.ValueList) > 0 {
|
|
||||||
return formatLiteral(p.ValueList[0])
|
|
||||||
}
|
|
||||||
dtype := strings.ToLower(firstType(p))
|
|
||||||
switch {
|
|
||||||
case strings.Contains(dtype, "bool"):
|
|
||||||
return "false"
|
|
||||||
case strings.Contains(dtype, "int") || strings.Contains(dtype, "number"):
|
|
||||||
return "1"
|
|
||||||
case strings.Contains(dtype, "json"):
|
|
||||||
return "{}"
|
|
||||||
}
|
|
||||||
return "\"TODO\""
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatLiteral(value interface{}) string {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case bool:
|
|
||||||
if v {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return "false"
|
|
||||||
case int:
|
|
||||||
return strconv.Itoa(v)
|
|
||||||
case int64:
|
|
||||||
return strconv.FormatInt(v, 10)
|
|
||||||
case float64:
|
|
||||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
|
||||||
case string:
|
|
||||||
if strings.HasPrefix(v, "{") || strings.HasPrefix(v, "[") {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(v, "\"") && strings.HasSuffix(v, "\"") {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if looksLikeNumber(v) || v == "true" || v == "false" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("\"%s\"", v)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("\"%v\"", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func looksLikeNumber(value string) bool {
|
|
||||||
if value == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if value[0] == '-' {
|
|
||||||
value = value[1:]
|
|
||||||
}
|
|
||||||
for _, ch := range value {
|
|
||||||
if ch == '.' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ch < '0' || ch > '9' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatID(id int) string {
|
|
||||||
if id == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strconv.Itoa(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatTypeCell(p ParamSpec) string {
|
|
||||||
dtype := firstType(p)
|
|
||||||
if dtype == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "<code>" + escapeText(dtype) + "</code>"
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstType(p ParamSpec) string {
|
|
||||||
if p.DataType != "" {
|
|
||||||
return normalizeType(p.DataType)
|
|
||||||
}
|
|
||||||
return normalizeType(p.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeType(dtype string) string {
|
|
||||||
cleaned := strings.ReplaceAll(dtype, ">", ">")
|
|
||||||
if idx := strings.Index(cleaned, ">"); idx >= 0 {
|
|
||||||
cleaned = cleaned[:idx]
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(cleaned)
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultCell(value interface{}) string {
|
|
||||||
if !hasDefault(value) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "<code>" + escapeText(formatLiteral(value)) + "</code>"
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickText(p ParamSpec) string {
|
|
||||||
if p.Descr == "" && p.Man == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if p.Man == "" {
|
|
||||||
return p.Descr
|
|
||||||
}
|
|
||||||
if p.Descr == "" {
|
|
||||||
return p.Man
|
|
||||||
}
|
|
||||||
if len(p.Man) > len(p.Descr) {
|
|
||||||
return p.Man
|
|
||||||
}
|
|
||||||
return p.Descr
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectConstraints(p ParamSpec) string {
|
|
||||||
parts := []string{}
|
|
||||||
if p.MinValue != nil {
|
|
||||||
parts = append(parts, fmt.Sprintf("minvalue=%d", *p.MinValue))
|
|
||||||
}
|
|
||||||
if p.MaxValue != nil {
|
|
||||||
parts = append(parts, fmt.Sprintf("maxvalue=%d", *p.MaxValue))
|
|
||||||
}
|
|
||||||
if p.Regex != "" {
|
|
||||||
parts = append(parts, fmt.Sprintf("regex=%s", p.Regex))
|
|
||||||
}
|
|
||||||
if len(p.ValueList) > 0 {
|
|
||||||
parts = append(parts, fmt.Sprintf("value_list=%s", strings.Join(p.ValueList, ", ")))
|
|
||||||
}
|
|
||||||
if p.Func != "" {
|
|
||||||
parts = append(parts, fmt.Sprintf("func=%s", p.Func))
|
|
||||||
}
|
|
||||||
if p.RefSvcID != nil {
|
|
||||||
parts = append(parts, fmt.Sprintf("ref_svc_id=%d", *p.RefSvcID))
|
|
||||||
}
|
|
||||||
if p.Unique != "" {
|
|
||||||
parts = append(parts, fmt.Sprintf("unique_scope=%s", p.Unique))
|
|
||||||
}
|
|
||||||
if p.MaxLength != nil {
|
|
||||||
parts = append(parts, fmt.Sprintf("maxlength=%d", *p.MaxLength))
|
|
||||||
}
|
|
||||||
if p.MinLength != nil {
|
|
||||||
parts = append(parts, fmt.Sprintf("minlength=%d", *p.MinLength))
|
|
||||||
}
|
|
||||||
return strings.Join(parts, "; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func escapeText(value string) string {
|
|
||||||
if value == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
text := html.EscapeString(value)
|
|
||||||
text = strings.ReplaceAll(text, "<br/>", "<br/>")
|
|
||||||
text = strings.ReplaceAll(text, "<br />", "<br/>")
|
|
||||||
text = strings.ReplaceAll(text, "<br>", "<br/>")
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
func escapeCode(value string) string {
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
for _, line := range strings.Split(value, "\n") {
|
|
||||||
buf.WriteString(html.EscapeString(line))
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatCode(code string) string {
|
|
||||||
if code == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if len(code) <= 40 {
|
|
||||||
return "<strong><code class=\"code-no-wrap\">" + code + "</code></strong>"
|
|
||||||
}
|
|
||||||
out := []string{}
|
|
||||||
prev := rune(0)
|
|
||||||
for _, ch := range code {
|
|
||||||
if ch == '_' {
|
|
||||||
out = append(out, "_<wbr>")
|
|
||||||
prev = ch
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if prev != 0 && (unicodeIsLower(prev) || unicodeIsDigit(prev)) && unicodeIsUpper(ch) {
|
|
||||||
out = append(out, "<wbr>")
|
|
||||||
}
|
|
||||||
out = append(out, string(ch))
|
|
||||||
prev = ch
|
|
||||||
}
|
|
||||||
return "<strong><code class=\"code-wrap\">" + strings.Join(out, "") + "</code></strong>"
|
|
||||||
}
|
|
||||||
|
|
||||||
func unicodeIsLower(r rune) bool { return r >= 'a' && r <= 'z' }
|
|
||||||
func unicodeIsUpper(r rune) bool { return r >= 'A' && r <= 'Z' }
|
|
||||||
func unicodeIsDigit(r rune) bool { return r >= '0' && r <= '9' }
|
|
||||||
|
|
||||||
func stripHTML(value string) string {
|
|
||||||
if value == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
re := regexp.MustCompile("<[^>]+>")
|
|
||||||
cleaned := re.ReplaceAllString(value, " ")
|
|
||||||
cleaned = strings.Join(strings.Fields(cleaned), " ")
|
|
||||||
return html.UnescapeString(cleaned)
|
|
||||||
}
|
|
||||||
|
|
||||||
func slug(value string) string {
|
|
||||||
out := []rune{}
|
|
||||||
for _, ch := range value {
|
|
||||||
if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' {
|
|
||||||
out = append(out, ch)
|
|
||||||
} else if ch >= 'A' && ch <= 'Z' {
|
|
||||||
out = append(out, ch+'a'-'A')
|
|
||||||
} else {
|
|
||||||
out = append(out, '_')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Trim(string(out), "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileExists(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user