- fetch_mcp_metadata: extract run()/resolveToken()/writeMetadata with injectable deps (keychain, servers, lister, registry, exit); full-path tests reach 100% file coverage. - internal/cli: drop dead initSafetyByCLIPath (superseded by ResolveMeta), split buildMetaByCLIPath / assembleSchemaCatalogSnapshot / assembleCommandRegistryFrom / mergedCommandRegistryJSON so shard and malformed-snapshot failure modes are testable; cover catalog structure violation formatting (sort/truncate) and RenderSafetyAnnotation. - generators: cover registry shard merge and catalog shard write failure modes. - helpers/cmdutil: cover LeafSpec default/server dispatch, transform error propagation, default env hint, devapp member remove validate chain, and the required-flags error helpers. Local gate: overall 90.17% vs merge-base 89.96%, changed-code 100% (861 statements); make policy and go test ./... green.
495 lines
18 KiB
Go
495 lines
18 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"embed"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const SchemaCatalogSnapshotVersion = 1
|
|
|
|
// The release Catalog is committed as a per-product split so concurrent
|
|
// feature PRs only rewrite the shard for the product they touch:
|
|
//
|
|
// schema_catalog/catalog.json global envelope + Catalog map
|
|
// schema_catalog/tools/<product>.json that product's leaf ToolSpecs
|
|
//
|
|
// The loader reassembles the exact same SchemaCatalogSnapshot, so the
|
|
// source_hash integrity check is identical to the single-file layout.
|
|
//
|
|
//go:embed schema_catalog/catalog.json
|
|
var embeddedSchemaCatalogEnvelopeJSON []byte
|
|
|
|
//go:embed schema_catalog/tools
|
|
var embeddedSchemaCatalogTools embed.FS
|
|
|
|
// SchemaCatalogSnapshot is the release-stable Agent contract. Catalog holds
|
|
// the progressive product/tool index; Tools holds full leaf parameter schemas.
|
|
// It intentionally contains no endpoint, credential, or runtime cache data.
|
|
type SchemaCatalogSnapshot struct {
|
|
Version int `json:"version"`
|
|
SourceHash string `json:"source_hash"`
|
|
SurfaceHash string `json:"surface_hash,omitempty"`
|
|
Catalog map[string]any `json:"catalog"`
|
|
Tools map[string]map[string]any `json:"tools"`
|
|
}
|
|
|
|
// SchemaCatalogBuildOptions carries release-envelope inputs which are checked
|
|
// against the effective reviewed CommandRegistry. The command set is not an
|
|
// option: visibility is resolved by EffectiveCommandRegistry before assembly,
|
|
// and every public command must be delivered.
|
|
type SchemaCatalogBuildOptions struct {
|
|
RegistryHash string
|
|
}
|
|
|
|
type loadedSchemaCatalog struct {
|
|
Snapshot SchemaCatalogSnapshot
|
|
Registry SchemaRegistry
|
|
Index SchemaIndex
|
|
}
|
|
|
|
var (
|
|
runtimeEmbeddedSchemaCatalogOnce sync.Once
|
|
runtimeEmbeddedSchemaCatalog loadedSchemaCatalog
|
|
runtimeEmbeddedSchemaCatalogErr error
|
|
)
|
|
|
|
var runtimeEmbeddedSchemaCatalogLazyLoadCount atomic.Uint64
|
|
|
|
func embeddedSchemaCatalog() loadedSchemaCatalog {
|
|
runtimeEmbeddedSchemaCatalogOnce.Do(func() {
|
|
runtimeEmbeddedSchemaCatalogLazyLoadCount.Add(1)
|
|
runtimeEmbeddedSchemaCatalog, runtimeEmbeddedSchemaCatalogErr = assembleEmbeddedSchemaCatalog()
|
|
})
|
|
return runtimeEmbeddedSchemaCatalog
|
|
}
|
|
|
|
// schemaCatalogEnvelope is the global half of the split release Catalog. It
|
|
// mirrors the generator's envelope struct; the Catalog map and release hashes
|
|
// do not partition by product and stay in one file.
|
|
type schemaCatalogEnvelope struct {
|
|
Version int `json:"version"`
|
|
SurfaceHash string `json:"surface_hash,omitempty"`
|
|
SourceHash string `json:"source_hash"`
|
|
Catalog map[string]any `json:"catalog"`
|
|
}
|
|
|
|
// schemaCatalogToolShard mirrors the per-product shard written by the
|
|
// generator. Only the product and its leaf ToolSpecs are stored here.
|
|
type schemaCatalogToolShard struct {
|
|
Product string `json:"product"`
|
|
Tools map[string]map[string]any `json:"tools"`
|
|
}
|
|
|
|
// assembleEmbeddedSchemaCatalog reassembles the split release Catalog shards
|
|
// into the same SchemaCatalogSnapshot the single-file layout produced, then
|
|
// validates it through the production loader. source_hash still covers the
|
|
// whole payload: if any shard is missing, stale, or tampered, the content hash
|
|
// check in loadSchemaCatalogSnapshot fails exactly as before.
|
|
func assembleEmbeddedSchemaCatalog() (loadedSchemaCatalog, error) {
|
|
snapshot, err := assembleSchemaCatalogSnapshot(embeddedSchemaCatalogEnvelopeJSON, embeddedSchemaCatalogTools, "schema_catalog/tools")
|
|
if err != nil {
|
|
return loadedSchemaCatalog{}, err
|
|
}
|
|
return loadSchemaCatalogSnapshot(snapshot)
|
|
}
|
|
|
|
// assembleSchemaCatalogSnapshot merges an envelope document and a directory of
|
|
// per-product tool shards back into the single-document snapshot shape. Split
|
|
// from assembleEmbeddedSchemaCatalog so shard failure modes stay testable
|
|
// against fake filesystems.
|
|
func assembleSchemaCatalogSnapshot(envelopeJSON []byte, shards fs.FS, dir string) (SchemaCatalogSnapshot, error) {
|
|
var envelope schemaCatalogEnvelope
|
|
if err := decodeStrictSchemaJSON(envelopeJSON, &envelope); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("decode embedded schema catalog.json: %w", err)
|
|
}
|
|
entries, err := fs.ReadDir(shards, dir)
|
|
if err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("read embedded schema catalog tools directory: %w", err)
|
|
}
|
|
tools := make(map[string]map[string]any, len(entries)*8)
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
|
continue
|
|
}
|
|
data, readErr := fs.ReadFile(shards, dir+"/"+entry.Name())
|
|
if readErr != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("read embedded schema catalog shard %s: %w", entry.Name(), readErr)
|
|
}
|
|
var shard schemaCatalogToolShard
|
|
if err := decodeStrictSchemaJSON(data, &shard); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("decode embedded schema catalog shard %s: %w", entry.Name(), err)
|
|
}
|
|
for canonical, spec := range shard.Tools {
|
|
tools[canonical] = spec
|
|
}
|
|
}
|
|
return SchemaCatalogSnapshot{
|
|
Version: envelope.Version,
|
|
SurfaceHash: envelope.SurfaceHash,
|
|
SourceHash: envelope.SourceHash,
|
|
Catalog: envelope.Catalog,
|
|
Tools: tools,
|
|
}, nil
|
|
}
|
|
|
|
func embeddedSchemaCatalogError() error {
|
|
_ = embeddedSchemaCatalog()
|
|
return runtimeEmbeddedSchemaCatalogErr
|
|
}
|
|
|
|
var (
|
|
buildCatalogValidateParameterBindings = ValidateSchemaParameterBindingDelivery
|
|
buildCatalogValidateDryRun = ValidateReviewedDryRunCapabilityDelivery
|
|
buildCatalogValidateExamples = ValidateEmbeddedManualAgentExampleDelivery
|
|
buildCatalogValidateCompleteness = validateResolvedRuntimeSchemaCompleteness
|
|
buildCatalogValidateRegistry = validateSchemaRegistryAgainstCommandRegistry
|
|
buildCatalogValidateInterfaces = validateSchemaRegistryInterfaces
|
|
buildCatalogValidateAgentMetadata = validateSchemaRegistryAgentMetadata
|
|
buildCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage
|
|
buildCatalogValidateDelivery = ValidateSchemaDeliveryInvariants
|
|
buildCatalogValidateFinalCompleteness = validateResolvedSchemaCatalogDeliveryCompleteness
|
|
|
|
loadCatalogValidateInterfaces = validateSchemaRegistryInterfaces
|
|
loadCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage
|
|
loadCatalogValidateAgentMetadata = validateSchemaRegistryAgentMetadata
|
|
|
|
renderEmbeddedSchemaAll = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToPayload() }
|
|
renderEmbeddedSchemaOverview = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToOverviewPayload() }
|
|
renderSchemaProductSummary = func(product ProductSpec) (map[string]any, error) { return product.ToSummaryPayload() }
|
|
renderSchemaToolSummary = func(tool ToolSpec) (map[string]any, error) { return tool.ToSummaryPayload() }
|
|
)
|
|
|
|
// BuildSchemaCatalogSnapshot renders a deterministic Catalog from one
|
|
// resolved source-to-delivery hand-off. It deliberately accepts no Cobra root:
|
|
// reapplying manual hints or rebuilding SchemaRegistry at this boundary would
|
|
// allow generation gates to validate one candidate while publishing another.
|
|
func BuildSchemaCatalogSnapshot(resolved ResolvedSchemaBuild, options SchemaCatalogBuildOptions) (SchemaCatalogSnapshot, error) {
|
|
if resolved.root == nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("schema Catalog source was not created by ResolveSchemaBuild")
|
|
}
|
|
registry := resolved.registry
|
|
effectiveCommands := resolved.effective
|
|
registryHash := strings.TrimSpace(options.RegistryHash)
|
|
if registryHash == "" {
|
|
registryHash = effectiveCommands.SourceHash()
|
|
} else if registryHash != effectiveCommands.SourceHash() {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("provided Registry hash %q disagrees with effective CommandRegistry %q", registryHash, effectiveCommands.SourceHash())
|
|
}
|
|
if err := buildCatalogValidateParameterBindings(resolved.bound, registry); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema parameter binding delivery: %w", err)
|
|
}
|
|
if err := buildCatalogValidateDryRun(registry); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate reviewed dry-run capability delivery: %w", err)
|
|
}
|
|
if _, err := buildCatalogValidateExamples(resolved.bound, registry); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Manual Agent example delivery: %w", err)
|
|
}
|
|
if err := buildCatalogValidateCompleteness(resolved.root, resolved.bound); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate reverse command-tree completeness: %w", err)
|
|
}
|
|
if err := buildCatalogValidateRegistry(registry, effectiveCommands); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate typed Schema registry against reviewed CommandRegistry: %w", err)
|
|
}
|
|
if err := buildCatalogValidateInterfaces(registry); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema interface disposition: %w", err)
|
|
}
|
|
if err := buildCatalogValidateAgentMetadata(registry); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema Agent metadata set: %w", err)
|
|
}
|
|
if err := buildCatalogValidateProvenance(registry); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema provenance: %w", err)
|
|
}
|
|
// Visibility has already selected the complete public set in
|
|
// collectRuntimeSchemaEntriesFromBound. Do not apply a post-assembly
|
|
// allowlist: doing so could silently erase an otherwise valid reviewed
|
|
// manual-only command after the exact-set validation above has passed.
|
|
registry.Source = "embedded-command-catalog"
|
|
payload, err := registry.ToSnapshotPayload()
|
|
if err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("serialize typed Schema registry: %w", err)
|
|
}
|
|
|
|
snapshot := SchemaCatalogSnapshot{
|
|
Version: SchemaCatalogSnapshotVersion,
|
|
SurfaceHash: registryHash,
|
|
Catalog: payload.Catalog,
|
|
Tools: payload.Tools,
|
|
}
|
|
snapshot.SourceHash = schemaCatalogSnapshotHash(snapshot)
|
|
if err := buildCatalogValidateDelivery(registry, snapshot); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Schema delivery invariants: %w", err)
|
|
}
|
|
if err := buildCatalogValidateFinalCompleteness(resolved.root, resolved.bound, snapshot); err != nil {
|
|
return SchemaCatalogSnapshot{}, fmt.Errorf("validate final Catalog delivery completeness: %w", err)
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
// decodeSchemaCatalogSnapshot is the single release and generation-time
|
|
// loading path. Delivery validation round-trips generated JSON through this
|
|
// function so it cannot pass with data that the shipped binary would reject.
|
|
func decodeSchemaCatalogSnapshot(data []byte) (loadedSchemaCatalog, error) {
|
|
var snapshot SchemaCatalogSnapshot
|
|
if err := decodeStrictSchemaJSON(data, &snapshot); err != nil {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("decode Schema Catalog snapshot: %w", err)
|
|
}
|
|
return loadSchemaCatalogSnapshot(snapshot)
|
|
}
|
|
|
|
// loadSchemaCatalogSnapshot constructs the production lookup from an
|
|
// arbitrary decoded snapshot. It validates the progressive summaries against
|
|
// the full leaf store before publishing any lookup key.
|
|
func loadSchemaCatalogSnapshot(snapshot SchemaCatalogSnapshot) (loadedSchemaCatalog, error) {
|
|
if snapshot.Version != SchemaCatalogSnapshotVersion {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("unsupported Schema Catalog snapshot version %d", snapshot.Version)
|
|
}
|
|
if len(snapshot.Catalog) == 0 || len(snapshot.Tools) == 0 {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot is empty")
|
|
}
|
|
if snapshot.SourceHash == "" || snapshot.SourceHash != schemaCatalogSnapshotHash(snapshot) {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("schema Catalog snapshot source_hash does not match its content")
|
|
}
|
|
registry, index, err := schemaRegistryFromSnapshot(snapshot)
|
|
if err != nil {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("load typed Schema registry: %w", err)
|
|
}
|
|
if err := loadCatalogValidateInterfaces(registry); err != nil {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema interface disposition: %w", err)
|
|
}
|
|
// The production loader validates delivered provenance exactly as encoded.
|
|
// toolSpecFromSnapshot deliberately does not synthesize candidates or
|
|
// rewrite winners, and this coverage gate applies to every snapshot source.
|
|
if err := loadCatalogValidateProvenance(registry); err != nil {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema provenance: %w", err)
|
|
}
|
|
if registry.Source == "embedded-command-catalog" {
|
|
if err := loadCatalogValidateAgentMetadata(registry); err != nil {
|
|
return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema Agent metadata set: %w", err)
|
|
}
|
|
}
|
|
return loadedSchemaCatalog{Snapshot: snapshot, Registry: registry, Index: index}, nil
|
|
}
|
|
|
|
func embeddedSchemaCatalogAvailable() bool {
|
|
return len(embeddedSchemaCatalog().Index.CanonicalPaths()) > 0
|
|
}
|
|
|
|
func embeddedSchemaAllPayload() (map[string]any, error) {
|
|
loaded := embeddedSchemaCatalog()
|
|
payload, err := renderEmbeddedSchemaAll(loaded.Registry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload["catalog_hash"] = loaded.Snapshot.SourceHash
|
|
if loaded.Snapshot.SurfaceHash != "" {
|
|
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func embeddedSchemaOverviewPayload() (map[string]any, error) {
|
|
loaded := embeddedSchemaCatalog()
|
|
payload, err := renderEmbeddedSchemaOverview(loaded.Registry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload["catalog_hash"] = loaded.Snapshot.SourceHash
|
|
if loaded.Snapshot.SurfaceHash != "" {
|
|
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func exactSchemaCommand(root *cobra.Command, rawPath string) *cobra.Command {
|
|
if root == nil {
|
|
return nil
|
|
}
|
|
parts := strings.Fields(strings.TrimSpace(rawPath))
|
|
if len(parts) > 0 && parts[0] == root.Name() {
|
|
parts = parts[1:]
|
|
}
|
|
current := root
|
|
for _, part := range parts {
|
|
var next *cobra.Command
|
|
for _, child := range current.Commands() {
|
|
if child.Name() == part {
|
|
next = child
|
|
break
|
|
}
|
|
}
|
|
if next == nil {
|
|
return nil
|
|
}
|
|
current = next
|
|
}
|
|
if current == root {
|
|
return nil
|
|
}
|
|
return current
|
|
}
|
|
|
|
func embeddedSchemaPayload(args []string) (map[string]any, error) {
|
|
return schemaPayloadFromLoadedCatalog(embeddedSchemaCatalog(), args)
|
|
}
|
|
|
|
// schemaPayloadFromLoadedCatalog is shared by the shipped schema command and
|
|
// the final-delivery gate. Keeping lookup and payload rendering on one path
|
|
// prevents generation-only validation from accepting an unqueryable snapshot.
|
|
func schemaPayloadFromLoadedCatalog(loaded loadedSchemaCatalog, args []string) (map[string]any, error) {
|
|
if len(args) == 0 {
|
|
snapshot, err := loaded.Registry.ToSnapshotPayload()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload := snapshot.Catalog
|
|
payload["catalog_hash"] = loaded.Snapshot.SourceHash
|
|
if loaded.Snapshot.SurfaceHash != "" {
|
|
payload["surface_hash"] = loaded.Snapshot.SurfaceHash
|
|
}
|
|
return payload, nil
|
|
}
|
|
raw := strings.TrimSpace(args[0])
|
|
if tool, ok := loaded.Index.ResolveQuery(raw); ok {
|
|
return schemaToolForResolvedPath(tool, raw).ToPayload()
|
|
}
|
|
tokens := splitSchemaPathTokens(raw)
|
|
if len(tokens) == 1 {
|
|
if product, ok := loaded.Index.Product(tokens[0]); ok {
|
|
payload, err := renderSchemaProductSummary(product)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{
|
|
"kind": "schema",
|
|
"level": "product",
|
|
"count": len(product.Tools),
|
|
"product": payload,
|
|
"source": "embedded-command-catalog",
|
|
}, nil
|
|
}
|
|
}
|
|
if len(tokens) > 1 {
|
|
path := strings.Join(tokens, " ")
|
|
if product, ok := loaded.Index.Product(tokens[0]); ok {
|
|
matched := make([]map[string]any, 0)
|
|
for _, tool := range product.Tools {
|
|
if schemaToolUnderGroup(tool, path) {
|
|
summary, err := renderSchemaToolSummary(tool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
matched = append(matched, summary)
|
|
}
|
|
}
|
|
if len(matched) > 0 {
|
|
return map[string]any{
|
|
"kind": "schema",
|
|
"level": "group",
|
|
"path": path,
|
|
"count": len(matched),
|
|
"tools": matched,
|
|
"source": "embedded-command-catalog",
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, apperrors.NewValidation("unknown runtime schema path " + strconvQuote(raw))
|
|
}
|
|
|
|
func schemaCatalogSnapshotHash(snapshot SchemaCatalogSnapshot) string {
|
|
payload := struct {
|
|
Version int `json:"version"`
|
|
SurfaceHash string `json:"surface_hash,omitempty"`
|
|
Catalog map[string]any `json:"catalog"`
|
|
Tools map[string]map[string]any `json:"tools"`
|
|
}{snapshot.Version, snapshot.SurfaceHash, snapshot.Catalog, snapshot.Tools}
|
|
encoded, _ := json.Marshal(payload)
|
|
sum := sha256.Sum256(encoded)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func schemaMapSlice(value any) []map[string]any {
|
|
switch values := value.(type) {
|
|
case []map[string]any:
|
|
return values
|
|
case []any:
|
|
out := make([]map[string]any, 0, len(values))
|
|
for _, value := range values {
|
|
if item, ok := value.(map[string]any); ok {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func schemaMap(value any) map[string]map[string]any {
|
|
input, ok := value.(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make(map[string]map[string]any, len(input))
|
|
for key, value := range input {
|
|
if item, ok := value.(map[string]any); ok {
|
|
out[key] = item
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func schemaString(value any) string {
|
|
valueString, _ := value.(string)
|
|
return valueString
|
|
}
|
|
|
|
func schemaStringSlice(value any) []string {
|
|
switch values := value.(type) {
|
|
case []string:
|
|
return values
|
|
case []any:
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if item, ok := value.(string); ok {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
return out
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func firstNonEmptySchemaString(values ...any) string {
|
|
for _, value := range values {
|
|
if text := strings.TrimSpace(schemaString(value)); text != "" {
|
|
return text
|
|
}
|
|
}
|
|
return ""
|
|
}
|