65e939889e
- ParseAndAssemble 加 validateTopology:拦重复/空节点 id + 悬挂边
(source/target 指向不存在节点),避免坏图进编排后被 compose 静默跳过
- 保守策略:空图 / 报告任务 {topic} / 非标准载荷一律宽松放过,不误伤合法提交
- 单测覆盖合法/重复id/空id/悬挂边×2/空端点/放过场景
- live:悬挂边 POST /tasks → 422(错误指明具体边与缺失节点);合法图 → 202
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
2.5 KiB
Go
82 lines
2.5 KiB
Go
// Package dsl 负责把客户端导出的 JSON DSL 解析并组装为可调度的 Task。
|
|
package dsl
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/sundynix/sundynix-shared/contract"
|
|
)
|
|
|
|
// ParseAndAssemble 校验 DSL 结构并生成共享契约中的 Task。
|
|
func ParseAndAssemble(raw json.RawMessage) (*contract.Task, error) {
|
|
if len(raw) == 0 {
|
|
return nil, errors.New("empty dsl")
|
|
}
|
|
// 轻量结构校验:至少要能解析为对象。
|
|
var probe map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &probe); err != nil {
|
|
return nil, errors.New("invalid dsl json: " + err.Error())
|
|
}
|
|
// 拓扑校验:前置拦截明确的结构错误(重复/空 id、悬挂边),
|
|
// 避免坏图进编排后被 compose 静默跳过(dangling edge 会被 nodeByID 检查悄悄丢弃)。
|
|
if err := validateTopology(raw); err != nil {
|
|
return nil, err
|
|
}
|
|
return &contract.Task{
|
|
ID: newID(),
|
|
Graph: raw,
|
|
Meta: map[string]any{},
|
|
}, nil
|
|
}
|
|
|
|
// validateTopology 做保守的拓扑校验:只拦不含糊的结构错误,对空图/非标准图(如报告任务的
|
|
// {topic:...})与无法按图结构解析的载荷一律宽松放过,杜绝误伤合法提交。
|
|
// - 节点 id 不得为空、不得重复
|
|
// - 每条边的 source/target 不得为空、且必须指向存在的节点(悬挂边直接拒)
|
|
func validateTopology(raw json.RawMessage) error {
|
|
var g struct {
|
|
Nodes []struct {
|
|
ID string `json:"id"`
|
|
} `json:"nodes"`
|
|
Edges []struct {
|
|
Source string `json:"source"`
|
|
Target string `json:"target"`
|
|
} `json:"edges"`
|
|
}
|
|
if err := json.Unmarshal(raw, &g); err != nil {
|
|
return nil // 非标准图结构(字段类型不匹配)→ 跳过拓扑校验,不误伤
|
|
}
|
|
ids := make(map[string]bool, len(g.Nodes))
|
|
for i, n := range g.Nodes {
|
|
if n.ID == "" {
|
|
return fmt.Errorf("dsl 校验失败:节点[%d] 缺少 id", i)
|
|
}
|
|
if ids[n.ID] {
|
|
return fmt.Errorf("dsl 校验失败:节点 id 重复:%s", n.ID)
|
|
}
|
|
ids[n.ID] = true
|
|
}
|
|
for i, e := range g.Edges {
|
|
if e.Source == "" || e.Target == "" {
|
|
return fmt.Errorf("dsl 校验失败:边[%d] 的 source/target 不能为空", i)
|
|
}
|
|
if !ids[e.Source] {
|
|
return fmt.Errorf("dsl 校验失败:边[%d] 引用不存在的源节点:%s", i, e.Source)
|
|
}
|
|
if !ids[e.Target] {
|
|
return fmt.Errorf("dsl 校验失败:边[%d] 引用不存在的目标节点:%s", i, e.Target)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newID() string {
|
|
var b [8]byte
|
|
_, _ = rand.Read(b[:])
|
|
return "task_" + hex.EncodeToString(b[:])
|
|
}
|