Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions cmd/umctl/cmd/umodel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"

Expand All @@ -11,6 +12,7 @@ import (
"github.com/alibaba/UnifiedModel/cmd/umctl/pkg/hint"
"github.com/alibaba/UnifiedModel/cmd/umctl/pkg/registry"
"github.com/alibaba/UnifiedModel/cmd/umctl/pkg/response"
"github.com/alibaba/UnifiedModel/internal/umodel/payload"
)

var umodelCmd = &cobra.Command{
Expand Down Expand Up @@ -51,11 +53,7 @@ var umodelValidateCmd = &cobra.Command{
file = rest[0]
}
requireFile(file)
raw, err := readRawFile(file)
if err != nil {
hint.HandleInputError(err, file)
}
raw, err = wrapPayload(raw, "elements")
raw, err := readUModelValidatePayload(file)
if err != nil {
hint.HandleInputError(err, file)
}
Expand Down Expand Up @@ -135,7 +133,7 @@ func init() {
addWorkspaceFlag(umodelImportCmd, umodelValidateCmd, umodelPutCmd, umodelDeleteCmd, umodelExportCmd)

umodelImportCmd.Flags().String("path", "", "Path to YAML/JSON file or directory")
umodelValidateCmd.Flags().StringP("file", "f", "", "JSON file with UModel elements")
umodelValidateCmd.Flags().StringP("file", "f", "", "JSON elements payload or single UModel YAML file")
umodelPutCmd.Flags().StringP("file", "f", "", "JSON file with UModel elements")
umodelDeleteCmd.Flags().String("ids", "", "Comma-separated element IDs")
umodelExportCmd.Flags().Int("limit", 1000, "Maximum number of elements to export")
Expand All @@ -153,7 +151,7 @@ func init() {
Description: "Validate UModel elements",
Params: []registry.ParamMeta{
{Name: "workspace", Type: "string", Flag: "--workspace", Short: "-w", Description: "Target workspace", Required: true},
{Name: "file", Type: "string", Flag: "--file", Short: "-f", Description: "JSON file with elements", Required: true},
{Name: "file", Type: "string", Flag: "--file", Short: "-f", Description: "JSON elements payload or single UModel YAML file", Required: true},
},
})
registry.Global.Register(registry.CommandMeta{
Expand Down Expand Up @@ -199,6 +197,29 @@ func buildIDsPayload(ids, reason string) []byte {
return b
}

func readUModelValidatePayload(file string) ([]byte, error) {
raw, err := readRawFile(file)
if err != nil {
return nil, err
}
if !isYAMLPath(file) {
return wrapPayload(raw, "elements")
}
element, err := payload.ParseElementBytes(raw, filepath.Ext(file))
if err != nil {
return nil, err
}
return marshalJSONBytes(map[string]any{"elements": []any{element}})
}

func isYAMLPath(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".yaml", ".yml":
return true
default:
return false
}
}
func requireWorkspace(ws string) {
if ws == "" {
response.ExitWithError(response.ExitParam, "Missing --workspace / -w flag", "Specify the target workspace.")
Expand Down
55 changes: 55 additions & 0 deletions cmd/umctl/cmd/umodel_yaml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"net/http"
"os"
"path/filepath"
"testing"
)

func TestUModelValidateAcceptsSchemaStyleYAMLFile(t *testing.T) {
yamlPath := filepath.Join(t.TempDir(), "service.yaml")
if err := os.WriteFile(yamlPath, []byte(`
kind: entity_set
schema:
version: v0.1.0
metadata:
name: devops.service
domain: devops
spec:
fields:
- name: id
type: string
`), 0o644); err != nil {
t.Fatalf("write yaml: %v", err)
}

cleanup := setupTestEnv(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertMethod(t, r, http.MethodPost)
assertPath(t, r, "/api/v1/umodel/demo/validate")
body := decodeBody(t, r)
elements, ok := body["elements"].([]any)
if !ok || len(elements) != 1 {
t.Fatalf("expected one normalized element, got %+v", body)
}
element, ok := elements[0].(map[string]any)
if !ok {
t.Fatalf("expected object element, got %+v", elements[0])
}
assertField(t, element, "kind", "entity_set")
assertField(t, element, "domain", "devops")
assertField(t, element, "name", "devops.service")
assertField(t, element, "version", "v0.1.0")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"valid":true}`))
}))
defer cleanup()

out, code := captureStdoutAndExitCode(t, func() {
rootCmd.SetArgs([]string{"umodel", "validate", "demo", yamlPath})
rootCmd.Execute()
})
if code != 0 {
t.Fatalf("exit code = %d, output = %s", code, out)
}
}
4 changes: 2 additions & 2 deletions docs/en/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ go run ./cmd/umctl --addr http://localhost:8080 workspace delete demo
## UModel

```bash
go run ./cmd/umctl --addr http://localhost:8080 umodel validate demo examples/quickstart-multidomain/devops/entity_set/devops.service.yaml
go run ./cmd/umctl --addr http://localhost:8080 umodel validate demo examples/quickstart-multidomain/umodel/devops/entity_set/devops.service.yaml
go run ./cmd/umctl --addr http://localhost:8080 umodel put demo '{"kind":"entity_set","domain":"devops","name":"devops.service"}'
go run ./cmd/umctl --addr http://localhost:8080 umodel import demo examples/quickstart-multidomain
go run ./cmd/umctl --addr http://localhost:8080 umodel export demo 100
```

`umodel put` and `umodel validate` accept a JSON object, a JSON array, or a JSON payload with an `elements` field.
`umodel validate` accepts a JSON object, JSON array, JSON payload with an `elements` field, or one UModel YAML source file. `umodel put` accepts JSON only: an object, array, or payload with an `elements` field.

## EntityStore

Expand Down
4 changes: 2 additions & 2 deletions docs/zh/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ go run ./cmd/umctl --addr http://localhost:8080 workspace delete demo
## UModel

```bash
go run ./cmd/umctl --addr http://localhost:8080 umodel validate demo examples/quickstart-multidomain/devops/entity_set/devops.service.yaml
go run ./cmd/umctl --addr http://localhost:8080 umodel validate demo examples/quickstart-multidomain/umodel/devops/entity_set/devops.service.yaml
go run ./cmd/umctl --addr http://localhost:8080 umodel put demo '{"kind":"entity_set","domain":"devops","name":"devops.service"}'
go run ./cmd/umctl --addr http://localhost:8080 umodel import demo examples/quickstart-multidomain
go run ./cmd/umctl --addr http://localhost:8080 umodel export demo 100
```

`umodel put` 和 `umodel validate` 接受 JSON object、JSON array,或包含 `elements` 字段的 payload。
`umodel validate` 接受 JSON object、JSON array、包含 `elements` 字段的 JSON payload,或单个 UModel YAML 源文件。`umodel put` 只接受 JSONobject、array,或包含 `elements` 字段的 payload。

## EntityStore

Expand Down
115 changes: 2 additions & 113 deletions internal/umodel/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ package umodel

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/alibaba/UnifiedModel/internal/umodel/payload"
apperrors "github.com/alibaba/UnifiedModel/pkg/errors"
"github.com/alibaba/UnifiedModel/pkg/model"
"gopkg.in/yaml.v3"
)

// Import loads UModel elements from an operator-provided path. The path is
Expand Down Expand Up @@ -78,7 +76,7 @@ func (s *Service) importInternal(ctx context.Context, workspace string, req mode
}
path = resolved
}
element, err := parseUModelElementFile(path)
element, err := payload.ParseElementFile(path)
if err != nil {
return result, apperrors.WithDetails(apperrors.CodeValidationFailed, "umodel import failed", map[string]string{
"path": path,
Expand Down Expand Up @@ -249,112 +247,3 @@ func isImportFile(path string) bool {
return false
}
}

func parseUModelElementFile(path string) (model.UModelElement, error) {
body, err := os.ReadFile(path)
if err != nil {
return model.UModelElement{}, err
}
var payload map[string]any
switch strings.ToLower(filepath.Ext(path)) {
case ".json":
if err := json.Unmarshal(body, &payload); err != nil {
return model.UModelElement{}, err
}
case ".yaml", ".yml":
if err := yaml.Unmarshal(body, &payload); err != nil {
return model.UModelElement{}, err
}
default:
return model.UModelElement{}, fmt.Errorf("unsupported file extension")
}
return elementFromPayload(normalizeMap(payload))
}

func elementFromPayload(payload map[string]any) (model.UModelElement, error) {
metadata := nestedMap(payload, "metadata")
schema := nestedMap(payload, "schema")

kind := firstString(payload["kind"])
name := firstString(metadata["name"], payload["name"])
domain := firstString(metadata["domain"], payload["domain"])
version := firstString(schema["version"], payload["version"])
if domain == "" {
domain = inferDomain(name)
}
if kind == "" {
return model.UModelElement{}, fmt.Errorf("kind is required")
}
if domain == "" {
return model.UModelElement{}, fmt.Errorf("domain or metadata.domain is required")
}
if name == "" {
return model.UModelElement{}, fmt.Errorf("name or metadata.name is required")
}

spec := nestedMap(payload, "spec")
return model.UModelElement{
Kind: kind,
Domain: domain,
Name: name,
Version: version,
Spec: spec,
}, nil
}

func normalizeMap(source map[string]any) map[string]any {
if source == nil {
return nil
}
out := make(map[string]any, len(source))
for key, value := range source {
out[key] = normalizeValue(value)
}
return out
}

func normalizeValue(value any) any {
switch typed := value.(type) {
case map[string]any:
return normalizeMap(typed)
case map[any]any:
out := make(map[string]any, len(typed))
for key, value := range typed {
out[fmt.Sprint(key)] = normalizeValue(value)
}
return out
case []any:
out := make([]any, len(typed))
for i, item := range typed {
out[i] = normalizeValue(item)
}
return out
default:
return typed
}
}

func nestedMap(source map[string]any, key string) map[string]any {
value, ok := source[key].(map[string]any)
if !ok {
return nil
}
return value
}

func firstString(values ...any) string {
for _, value := range values {
text, ok := value.(string)
if ok && text != "" {
return text
}
}
return ""
}

func inferDomain(name string) string {
if before, _, ok := strings.Cut(name, "."); ok {
return before
}
return ""
}
Loading
Loading