Compare commits
No commits in common. "main" and "v1.5.7" have entirely different histories.
@ -1,11 +1,5 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## v1.5.8 (2026-08-19)
|
|
||||||
- **Action YAML**: 增加无状态解析、保注释 Token 补丁和 JSON 到 YAML 转换能力,不绑定存储或内置第三方服务定义。
|
|
||||||
- **可选 Timing**: `CallOptions.Timing` 按需返回毫秒级 `total`,流式调用额外返回首个响应数据块的 `firstToken`。
|
|
||||||
- **JavaScript 结果一致性**: `api.Call` 的完整结果统一投影为 lower-camel Map,避免 Go 结构体字段名泄露到低代码层。
|
|
||||||
- **依赖对齐**: 升级 `file`、`id`、`rand` 和 `shell` 到当前稳定补丁版本。
|
|
||||||
|
|
||||||
## v1.5.7 (2026-08-17)
|
## v1.5.7 (2026-08-17)
|
||||||
- **低代码结果投影**: 动态 Action 可通过 `resultMode: data` 让 JavaScript `api.Call` 直接返回业务数据,并补齐 `ok/code/error`。
|
- **低代码结果投影**: 动态 Action 可通过 `resultMode: data` 让 JavaScript `api.Call` 直接返回业务数据,并补齐 `ok/code/error`。
|
||||||
- **继承一致性**: 结果投影读取完整的 Action 继承配置,子 Action 无需重复声明。
|
- **继承一致性**: 结果投影读取完整的 Action 继承配置,子 Action 无需重复声明。
|
||||||
|
|||||||
@ -28,12 +28,11 @@ go get apigo.cc/go/api
|
|||||||
* `ConfigurableAction`:提供硬编码的默认参数或元数据。
|
* `ConfigurableAction`:提供硬编码的默认参数或元数据。
|
||||||
* `URLAction` / `MethodAction`:动态指定 Endpoint 和 HTTP 方法。
|
* `URLAction` / `MethodAction`:动态指定 Endpoint 和 HTTP 方法。
|
||||||
* `ValidatableAction`:业务参数自校验。
|
* `ValidatableAction`:业务参数自校验。
|
||||||
* `CallOptions`:提供内部 Token、超时、临时配置覆盖、Logger、流式回调与可选 Timing。
|
* `CallOptions`:提供内部 Token、超时、临时配置覆盖、Logger 与流式回调。
|
||||||
* `Result`:统一返回 `ok/statusCode/headers/data/code/error`,开启 Timing 时额外返回 `timing`;业务响应保留在 `data`。
|
* `Result`:统一返回 `ok/statusCode/headers/data/code/error`,业务响应保留在 `data`。
|
||||||
* `RegisterAction` / `RemoveAction`:动态注册和热更新数据驱动的 Action。
|
* `RegisterAction` / `RemoveAction`:动态注册和热更新数据驱动的 Action。
|
||||||
* `RegisterJSSigner` / `RemoveSigner`:管理 JavaScript Signer。
|
* `RegisterJSSigner` / `RemoveSigner`:管理 JavaScript Signer。
|
||||||
* `RegisterJSFilter` / `RemoveFilter`:管理可复用的 JavaScript 请求/响应 Filter。
|
* `RegisterJSFilter` / `RemoveFilter`:管理可复用的 JavaScript 请求/响应 Filter。
|
||||||
* `ParseActionYAML` / `PatchActionTokens` / `ConvertActionJSONToYAML`:无状态解析、保注释补丁和旧格式转换;不内置任何供应商 Action。
|
|
||||||
|
|
||||||
## 🔒 安全性 (Ultimate Memory Safety)
|
## 🔒 安全性 (Ultimate Memory Safety)
|
||||||
|
|
||||||
@ -77,8 +76,6 @@ result, err := api.CallBy("openai", payload, &api.CallOptions{
|
|||||||
|
|
||||||
`tokens` 未配置或为空时无需 Token。`tokens/enabled/test/logging` 与原配置中的密文字段不能通过 `options.config` 覆盖。流式调用使用 `go/http.ManualDo`,`OnHeaders` 与 `OnDone` 接收 lower-camel map,`OnData` 接收原始字节块。
|
`tokens` 未配置或为空时无需 Token。`tokens/enabled/test/logging` 与原配置中的密文字段不能通过 `options.config` 覆盖。流式调用使用 `go/http.ManualDo`,`OnHeaders` 与 `OnDone` 接收 lower-camel map,`OnData` 接收原始字节块。
|
||||||
|
|
||||||
调用时设置 `CallOptions.Timing=true` 可获得毫秒级粗粒度诊断信息:所有调用返回 `timing.total`,流式调用还返回从调用开始到首个响应数据块的 `timing.firstToken`。默认不采集也不返回 Timing;`firstToken` 表示首个网络数据块,并非协议解析后的精确模型 Token。
|
|
||||||
|
|
||||||
Action 只使用 `filters: ["name"]` 这一种形式。每个 Filter 都会收到前置 `request` 以及后置 `headers/chunk/result/done` 事件,并自行根据 `event` 决定是否处理。流式 Filter 应通过输入/输出的 `state` 保存单次调用状态,不能假设 HTTP chunk 与 SSE 或 JSONL 消息边界一致。JavaScript Filter 的 `chunk` 是 UTF-8 字符串;Go Filter 仍接收原始 `[]byte`,二进制响应不应挂载文本型 JavaScript Filter。
|
Action 只使用 `filters: ["name"]` 这一种形式。每个 Filter 都会收到前置 `request` 以及后置 `headers/chunk/result/done` 事件,并自行根据 `event` 决定是否处理。流式 Filter 应通过输入/输出的 `state` 保存单次调用状态,不能假设 HTTP chunk 与 SSE 或 JSONL 消息边界一致。JavaScript Filter 的 `chunk` 是 UTF-8 字符串;Go Filter 仍接收原始 `[]byte`,二进制响应不应挂载文本型 JavaScript Filter。
|
||||||
|
|
||||||
动态 Action 可通过 `extends` 继承另一个 Action;父配置先合并,子配置深度覆盖。继承在每次调用时解析,因此父 Action 热更新会立即作用于子 Action。循环继承或不存在的父 Action 会直接返回错误。
|
动态 Action 可通过 `extends` 继承另一个 Action;父配置先合并,子配置深度覆盖。继承在每次调用时解析,因此父 Action 热更新会立即作用于子 Action。循环继承或不存在的父 Action 会直接返回错误。
|
||||||
|
|||||||
12
TEST.md
12
TEST.md
@ -28,8 +28,8 @@
|
|||||||
### 5. 动态策略与统一结果 (`TestTokenAndOverridePolicy`, `TestResultRulesAndConfigOverride`)
|
### 5. 动态策略与统一结果 (`TestTokenAndOverridePolicy`, `TestResultRulesAndConfigOverride`)
|
||||||
验证 Action 内部 Token、控制字段黑名单、密文字段不可覆盖、URL/Method 临时覆盖,以及 `codeFields/successCodes/errorFields` 对统一结果的判断。
|
验证 Action 内部 Token、控制字段黑名单、密文字段不可覆盖、URL/Method 临时覆盖,以及 `codeFields/successCodes/errorFields` 对统一结果的判断。
|
||||||
|
|
||||||
### 6. 流式调用与可选 Timing (`TestStreamPreservesChunkOrder`, `TestOptionalTiming`)
|
### 6. 流式调用 (`TestStreamPreservesChunkOrder`)
|
||||||
使用 `ManualDo` 验证流式 Body 的分块顺序、同步回调、完成事件和统一结果状态;验证 Timing 默认关闭,开启后返回总耗时和流式首个响应数据块耗时。
|
使用 `ManualDo` 验证流式 Body 的分块顺序、同步回调、完成事件和统一结果状态。
|
||||||
|
|
||||||
### 7. 动态 Action 继承与统一 Filter 管线
|
### 7. 动态 Action 继承与统一 Filter 管线
|
||||||
|
|
||||||
@ -39,15 +39,11 @@
|
|||||||
|
|
||||||
验证 `resultMode: data` 仅向 JavaScript 返回业务数据与 `ok/code/error`,不会泄露 HTTP Headers;同时验证子 Action 能继承父 Action 的投影策略。
|
验证 `resultMode: data` 仅向 JavaScript 返回业务数据与 `ok/code/error`,不会泄露 HTTP Headers;同时验证子 Action 能继承父 Action 的投影策略。
|
||||||
|
|
||||||
### 9. Action YAML (`TestParseActionYAML`, `TestPatchActionTokensPreservesDocument`, `TestConvertActionJSONToYAML`)
|
|
||||||
|
|
||||||
验证 Action YAML 的对象解析、非对象拒绝、保留注释与字段顺序的 Token 补丁,以及不注入供应商默认值的 JSON 到 YAML 转换。
|
|
||||||
|
|
||||||
## ⏱ 性能基准测试 (Benchmark)
|
## ⏱ 性能基准测试 (Benchmark)
|
||||||
|
|
||||||
使用 `go test -bench=. ./...` 评估框架调用阶段的开销。
|
使用 `go test -bench=. ./...` 评估框架调用阶段的开销。
|
||||||
> **基准**: Darwin / Apple M3 Max
|
> **基准**: Darwin / Apple M3 Max
|
||||||
* `BenchmarkCallEngineLogic-16`:约 **117.2 ns/op**, **80 B/op**, **2 allocs/op**。
|
* `BenchmarkCallEngineLogic-16`:约 **115.4 ns/op**, **80 B/op**, **2 allocs/op**。
|
||||||
该指标证明引擎的参数合并、注入及校验流程具有极高的运行效率和极小的内存逃逸。
|
该指标证明引擎的参数合并、注入及校验流程具有极高的运行效率和极小的内存逃逸。
|
||||||
|
|
||||||
## 🚀 运行测试
|
## 🚀 运行测试
|
||||||
@ -61,5 +57,5 @@ go test -bench=. ./...
|
|||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
最后测试日期:2026-08-21
|
最后测试日期:2026-08-17
|
||||||
状态:PASS
|
状态:PASS
|
||||||
|
|||||||
@ -224,6 +224,5 @@ type Result struct {
|
|||||||
Data any
|
Data any
|
||||||
Code string
|
Code string
|
||||||
Error string
|
Error string
|
||||||
Timing map[string]any
|
|
||||||
receivedBytes int64
|
receivedBytes int64
|
||||||
}
|
}
|
||||||
|
|||||||
62
engine.go
62
engine.go
@ -25,12 +25,6 @@ func Call(action Action, options ...*CallOptions) (*Result, error) {
|
|||||||
opts := firstOptions(options)
|
opts := firstOptions(options)
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
result := &Result{Headers: map[string]string{}}
|
result := &Result{Headers: map[string]string{}}
|
||||||
finishTiming := func() {}
|
|
||||||
if opts.Timing {
|
|
||||||
result.Timing = map[string]any{"unit": "ms"}
|
|
||||||
finishTiming = func() { result.Timing["total"] = time.Since(started).Milliseconds() }
|
|
||||||
defer finishTiming()
|
|
||||||
}
|
|
||||||
actionConfig, _ := GetActionConfig(action.ActionName())
|
actionConfig, _ := GetActionConfig(action.ActionName())
|
||||||
if ca, ok := action.(ConfigurableAction); ok {
|
if ca, ok := action.(ConfigurableAction); ok {
|
||||||
MergeMap(actionConfig, ca.Config())
|
MergeMap(actionConfig, ca.Config())
|
||||||
@ -97,9 +91,6 @@ func Call(action Action, options ...*CallOptions) (*Result, error) {
|
|||||||
actionConfig = filteredConfig
|
actionConfig = filteredConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cast.Bool(actionConfig["llm"]) {
|
|
||||||
sanitizeLLMPayload(httpReq.Payload)
|
|
||||||
}
|
|
||||||
if err := sign(cast.String(actionConfig["signer"]), httpReq, actionConfig); err != nil {
|
if err := sign(cast.String(actionConfig["signer"]), httpReq, actionConfig); err != nil {
|
||||||
result.Error = "sign failed: " + err.Error()
|
result.Error = "sign failed: " + err.Error()
|
||||||
return result, errors.New(result.Error)
|
return result, errors.New(result.Error)
|
||||||
@ -113,7 +104,7 @@ func Call(action Action, options ...*CallOptions) (*Result, error) {
|
|||||||
client := gohttp.NewClient(timeout)
|
client := gohttp.NewClient(timeout)
|
||||||
defer client.Destroy()
|
defer client.Destroy()
|
||||||
if opts.Stream != nil {
|
if opts.Stream != nil {
|
||||||
err = callStream(client, httpReq, payload, opts.Stream, filters, action.ActionName(), opts.Context, result, started)
|
err = callStream(client, httpReq, payload, opts.Stream, filters, action.ActionName(), opts.Context, result)
|
||||||
} else {
|
} else {
|
||||||
err = callBuffered(client, httpReq, payload, result)
|
err = callBuffered(client, httpReq, payload, result)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -136,7 +127,6 @@ func Call(action Action, options ...*CallOptions) (*Result, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil && opts.Stream != nil && opts.Stream.OnDone != nil {
|
if err == nil && opts.Stream != nil && opts.Stream.OnDone != nil {
|
||||||
finishTiming()
|
|
||||||
err = opts.Stream.OnDone(resultMap(result))
|
err = opts.Stream.OnDone(resultMap(result))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Ok = false
|
result.Ok = false
|
||||||
@ -150,16 +140,6 @@ func Call(action Action, options ...*CallOptions) (*Result, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeLLMPayload(payload any) {
|
|
||||||
values, ok := payload.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, field := range []string{"name", "title", "extends", "abstract", "creation", "providerProtocol", "url", "host", "baseUrl", "path", "method", "format", "signer", "key", "filters", "requestSchema", "responseSchema", "test", "models", "defaultModel", "reasoningSupported"} {
|
|
||||||
delete(values, field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyActionTraits(action Action, config map[string]any) {
|
func applyActionTraits(action Action, config map[string]any) {
|
||||||
if value, ok := action.(URLAction); ok && value.GetURL() != "" {
|
if value, ok := action.(URLAction); ok && value.GetURL() != "" {
|
||||||
config["url"] = value.GetURL()
|
config["url"] = value.GetURL()
|
||||||
@ -271,7 +251,7 @@ func callBuffered(client *gohttp.Client, req *HttpRequest, payload any, result *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func callStream(client *gohttp.Client, req *HttpRequest, payload any, stream *StreamOptions, filters *filterPipeline, action string, ctx context.Context, result *Result, started time.Time) error {
|
func callStream(client *gohttp.Client, req *HttpRequest, payload any, stream *StreamOptions, filters *filterPipeline, action string, ctx context.Context, result *Result) error {
|
||||||
res := client.ManualDo(req.Method, req.Url, payload, headerSlice(req)...)
|
res := client.ManualDo(req.Method, req.Url, payload, headerSlice(req)...)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
result.Error = res.Error.Error()
|
result.Error = res.Error.Error()
|
||||||
@ -298,25 +278,18 @@ func callStream(client *gohttp.Client, req *HttpRequest, payload any, stream *St
|
|||||||
buf := make([]byte, 32*1024)
|
buf := make([]byte, 32*1024)
|
||||||
for {
|
for {
|
||||||
n, readErr := res.Response.Body.Read(buf)
|
n, readErr := res.Response.Body.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 && stream.OnData != nil {
|
||||||
if result.Timing != nil {
|
|
||||||
if _, exists := result.Timing["firstToken"]; !exists {
|
|
||||||
result.Timing["firstToken"] = time.Since(started).Milliseconds()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.receivedBytes += int64(n)
|
result.receivedBytes += int64(n)
|
||||||
if stream.OnData != nil {
|
filtered, filterErr := filters.apply(ctx, "chunk", map[string]any{"action": action, "chunk": append([]byte(nil), buf[:n]...), "drop": false})
|
||||||
filtered, filterErr := filters.apply(ctx, "chunk", map[string]any{"action": action, "chunk": append([]byte(nil), buf[:n]...), "drop": false})
|
if filterErr != nil {
|
||||||
if filterErr != nil {
|
result.Error = filterErr.Error()
|
||||||
result.Error = filterErr.Error()
|
return filterErr
|
||||||
return filterErr
|
}
|
||||||
}
|
chunk := filteredChunk(filtered["chunk"])
|
||||||
chunk := filteredChunk(filtered["chunk"])
|
if !cast.Bool(filtered["drop"]) && len(chunk) > 0 {
|
||||||
if !cast.Bool(filtered["drop"]) && len(chunk) > 0 {
|
if err := invokeStreamCallback(func() error { return stream.OnData(chunk) }); err != nil {
|
||||||
if err := invokeStreamCallback(func() error { return stream.OnData(chunk) }); err != nil {
|
result.Error = err.Error()
|
||||||
result.Error = err.Error()
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -332,17 +305,10 @@ func callStream(client *gohttp.Client, req *HttpRequest, payload any, stream *St
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resultMap(result *Result) map[string]any {
|
func resultMap(result *Result) map[string]any {
|
||||||
if result == nil {
|
return map[string]any{
|
||||||
return map[string]any{"ok": false, "statusCode": 0, "headers": map[string]string{}, "data": nil, "code": "", "error": "empty API result"}
|
|
||||||
}
|
|
||||||
output := map[string]any{
|
|
||||||
"ok": result.Ok, "statusCode": result.StatusCode, "headers": result.Headers,
|
"ok": result.Ok, "statusCode": result.StatusCode, "headers": result.Headers,
|
||||||
"data": result.Data, "code": result.Code, "error": result.Error,
|
"data": result.Data, "code": result.Code, "error": result.Error,
|
||||||
}
|
}
|
||||||
if result.Timing != nil {
|
|
||||||
output["timing"] = result.Timing
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func invokeStreamCallback(callback func() error) (err error) {
|
func invokeStreamCallback(callback func() error) (err error) {
|
||||||
|
|||||||
@ -63,27 +63,6 @@ func TestResultRulesAndConfigOverride(t *testing.T) {
|
|||||||
if receivedPath != "/v1/new" || receivedMethod != "PUT" {
|
if receivedPath != "/v1/new" || receivedMethod != "PUT" {
|
||||||
t.Fatalf("override reached %s %s", receivedMethod, receivedPath)
|
t.Fatalf("override reached %s %s", receivedMethod, receivedPath)
|
||||||
}
|
}
|
||||||
if result.Timing != nil {
|
|
||||||
t.Fatalf("timing must be omitted by default: %#v", result.Timing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOptionalTiming(t *testing.T) {
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
name := registerTestAction(t, map[string]any{"url": server.URL, "method": "GET"})
|
|
||||||
result, err := api.CallBy(name, nil, &api.CallOptions{Timing: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if result.Timing["unit"] != "ms" {
|
|
||||||
t.Fatalf("unexpected timing unit: %#v", result.Timing)
|
|
||||||
}
|
|
||||||
if _, ok := result.Timing["total"].(int64); !ok {
|
|
||||||
t.Fatalf("timing total is missing: %#v", result.Timing)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStreamPreservesChunkOrder(t *testing.T) {
|
func TestStreamPreservesChunkOrder(t *testing.T) {
|
||||||
@ -99,7 +78,7 @@ func TestStreamPreservesChunkOrder(t *testing.T) {
|
|||||||
name := registerTestAction(t, map[string]any{"url": server.URL, "method": "GET"})
|
name := registerTestAction(t, map[string]any{"url": server.URL, "method": "GET"})
|
||||||
var got bytes.Buffer
|
var got bytes.Buffer
|
||||||
var done bool
|
var done bool
|
||||||
result, err := api.CallBy(name, nil, &api.CallOptions{Timing: true, Stream: &api.StreamOptions{
|
result, err := api.CallBy(name, nil, &api.CallOptions{Stream: &api.StreamOptions{
|
||||||
OnData: func(data []byte) error { _, _ = got.Write(data); return nil },
|
OnData: func(data []byte) error { _, _ = got.Write(data); return nil },
|
||||||
OnDone: func(result map[string]any) error { done, _ = result["ok"].(bool); return nil },
|
OnDone: func(result map[string]any) error { done, _ = result["ok"].(bool); return nil },
|
||||||
}})
|
}})
|
||||||
@ -109,9 +88,6 @@ func TestStreamPreservesChunkOrder(t *testing.T) {
|
|||||||
if !result.Ok || !done || !reflect.DeepEqual(got.Bytes(), bytes.Join(chunks, nil)) {
|
if !result.Ok || !done || !reflect.DeepEqual(got.Bytes(), bytes.Join(chunks, nil)) {
|
||||||
t.Fatalf("stream mismatch: ok=%v done=%v body=%q", result.Ok, done, got.String())
|
t.Fatalf("stream mismatch: ok=%v done=%v body=%q", result.Ok, done, got.String())
|
||||||
}
|
}
|
||||||
if _, ok := result.Timing["firstToken"].(int64); !ok {
|
|
||||||
t.Fatalf("stream first-token timing is missing: %#v", result.Timing)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnifiedFilterLifecycle(t *testing.T) {
|
func TestUnifiedFilterLifecycle(t *testing.T) {
|
||||||
|
|||||||
12
go.mod
12
go.mod
@ -9,18 +9,18 @@ require (
|
|||||||
apigo.cc/go/encoding v1.5.6
|
apigo.cc/go/encoding v1.5.6
|
||||||
apigo.cc/go/http v1.5.4
|
apigo.cc/go/http v1.5.4
|
||||||
apigo.cc/go/jsmod v1.5.3
|
apigo.cc/go/jsmod v1.5.3
|
||||||
apigo.cc/go/log v1.5.9
|
|
||||||
apigo.cc/go/safe v1.5.3
|
apigo.cc/go/safe v1.5.3
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/file v1.5.6 // indirect
|
apigo.cc/go/file v1.5.5 // indirect
|
||||||
apigo.cc/go/id v1.5.7 // indirect
|
apigo.cc/go/id v1.5.4 // indirect
|
||||||
apigo.cc/go/rand v1.5.4 // indirect
|
apigo.cc/go/log v1.5.9 // indirect
|
||||||
apigo.cc/go/shell v1.5.5 // indirect
|
apigo.cc/go/rand v1.5.3 // indirect
|
||||||
|
apigo.cc/go/shell v1.5.3 // indirect
|
||||||
golang.org/x/crypto v0.52.0 // indirect
|
golang.org/x/crypto v0.52.0 // indirect
|
||||||
golang.org/x/net v0.54.0 // indirect
|
golang.org/x/net v0.54.0 // indirect
|
||||||
golang.org/x/sys v0.45.0 // indirect
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
30
go.sum
30
go.sum
@ -1,27 +1,41 @@
|
|||||||
|
apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw=
|
||||||
|
apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
|
||||||
apigo.cc/go/cast v1.5.5 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw=
|
apigo.cc/go/cast v1.5.5 h1:DMbfK3uPhPjRaXutj3StIZIkfjFIATSXfuAOeNOd4Fw=
|
||||||
apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
|
apigo.cc/go/cast v1.5.5/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
|
||||||
|
apigo.cc/go/config v1.5.3 h1:peq1FM2xO+vzPHJf8Dwg3DXm8PtFQMfTFKQj6fpoG7A=
|
||||||
|
apigo.cc/go/config v1.5.3/go.mod h1:ZiOAjWa1mQIzszaJZN+kO6YU4GXreng+NxkcK/TAkqQ=
|
||||||
apigo.cc/go/config v1.5.4 h1:1c/OarGwbz3+6ikurE+a6LJLjtzXvGvbxw3HV/Nk54M=
|
apigo.cc/go/config v1.5.4 h1:1c/OarGwbz3+6ikurE+a6LJLjtzXvGvbxw3HV/Nk54M=
|
||||||
apigo.cc/go/config v1.5.4/go.mod h1:oN+D2F8ETIyqKp+Yu8R4PRQlUoqR44o35jAHwLrrAq0=
|
apigo.cc/go/config v1.5.4/go.mod h1:oN+D2F8ETIyqKp+Yu8R4PRQlUoqR44o35jAHwLrrAq0=
|
||||||
|
apigo.cc/go/crypto v1.5.3 h1:2JUHC2cgR2zrnn36EzwkUAdxmmTXAA/8yTNo+2X1mPE=
|
||||||
|
apigo.cc/go/crypto v1.5.3/go.mod h1:PheYKHEXmoEFI1AK5PpY1borQWcRlkkSaWncT3cWbhE=
|
||||||
apigo.cc/go/crypto v1.5.5 h1:YQHumieqviNGMhwoxDtMuUdGWAQiGrkVhz9hZRHOhWs=
|
apigo.cc/go/crypto v1.5.5 h1:YQHumieqviNGMhwoxDtMuUdGWAQiGrkVhz9hZRHOhWs=
|
||||||
apigo.cc/go/crypto v1.5.5/go.mod h1:z/FXt0HE7fSMJKF2MF7jY2fSjlHUYYaSffETJ7ZzJmE=
|
apigo.cc/go/crypto v1.5.5/go.mod h1:z/FXt0HE7fSMJKF2MF7jY2fSjlHUYYaSffETJ7ZzJmE=
|
||||||
|
apigo.cc/go/encoding v1.5.4 h1:Fk8TrveZATyy8SHukC4ZiqdTSp+QIfsRHtt55xmMK7w=
|
||||||
|
apigo.cc/go/encoding v1.5.4/go.mod h1:dShEsZ3gKqBINz7TSOYf4e7/fBCqCY9VzlenoGUQUFM=
|
||||||
apigo.cc/go/encoding v1.5.6 h1:v02swVfbFGidD4QcX2ktuHHbCjdSbOB85fhzAXay+7M=
|
apigo.cc/go/encoding v1.5.6 h1:v02swVfbFGidD4QcX2ktuHHbCjdSbOB85fhzAXay+7M=
|
||||||
apigo.cc/go/encoding v1.5.6/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8=
|
apigo.cc/go/encoding v1.5.6/go.mod h1:Big9q1Zwy4071dXtnrQ3SJDzfa/G7/A60KE/5+M//P8=
|
||||||
apigo.cc/go/file v1.5.6 h1:Y7w3Tyu4e16VuED7rF2pzba+dzGE+hjDnBlIPVHIfzA=
|
apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs=
|
||||||
apigo.cc/go/file v1.5.6/go.mod h1:9sdW4ylSOA0HWc8Yt8qdnmMf6nn5SUEmjoPKyXpYXIQ=
|
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
|
||||||
|
apigo.cc/go/http v1.5.3 h1:nvJh9bqPPcPRv6p8WEw7bJAd0UC+r2zvQA8/QioVLTQ=
|
||||||
|
apigo.cc/go/http v1.5.3/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q=
|
||||||
apigo.cc/go/http v1.5.4 h1:Tm95WMsXyazFnaaPopdMXv1k9TiaPenLxDQ40WCrsEQ=
|
apigo.cc/go/http v1.5.4 h1:Tm95WMsXyazFnaaPopdMXv1k9TiaPenLxDQ40WCrsEQ=
|
||||||
apigo.cc/go/http v1.5.4/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q=
|
apigo.cc/go/http v1.5.4/go.mod h1:cFrPK61y9f1PrsNSJscZT/QVOgkT15o9OP7O8cuMb8Q=
|
||||||
apigo.cc/go/id v1.5.7 h1:Y5Sx6sQBCAdYMCQPTjODZyGMMd1+WRWCy2dWHVq11XQ=
|
apigo.cc/go/id v1.5.4 h1:D1Zx9gEZhOgdTgZ4SdmPImhpc9xGiOA33Y+j2MkstzQ=
|
||||||
apigo.cc/go/id v1.5.7/go.mod h1:fugudFBqfVNakfm91zZzuzU0P4PULzo6sylB8hRMqxA=
|
apigo.cc/go/id v1.5.4/go.mod h1:hCTQq+KC1ALWe1FpPERf+W4B6FSulg9FAgOUJDDySiY=
|
||||||
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
||||||
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||||
|
apigo.cc/go/log v1.5.8 h1:/IYtGPWhRjT3OayylDIphkWZIQbpLjqVeSnFEiD3Dy0=
|
||||||
|
apigo.cc/go/log v1.5.8/go.mod h1:HfFPANMYxJx197SSTXB21Pgxcz/gGqPP8nlSErgd5WE=
|
||||||
apigo.cc/go/log v1.5.9 h1:g8JehZrpVJyesesv+JFig5szEELsO4UjShsjlEihCfc=
|
apigo.cc/go/log v1.5.9 h1:g8JehZrpVJyesesv+JFig5szEELsO4UjShsjlEihCfc=
|
||||||
apigo.cc/go/log v1.5.9/go.mod h1:b4f/UB5Kk7oiFtkXvG4GVilmHGygWgxELRAdEZrIyfs=
|
apigo.cc/go/log v1.5.9/go.mod h1:b4f/UB5Kk7oiFtkXvG4GVilmHGygWgxELRAdEZrIyfs=
|
||||||
apigo.cc/go/rand v1.5.4 h1:eessFBsKQuoOYdzrStldOGw9f4HqbeO87X95bI4jIBQ=
|
apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
|
||||||
apigo.cc/go/rand v1.5.4/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
||||||
|
apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
|
||||||
|
apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
|
||||||
apigo.cc/go/safe v1.5.3 h1:9p/BmdlVWLbekpKByZIFC09Qn8Wdhik2eINiwunBxPs=
|
apigo.cc/go/safe v1.5.3 h1:9p/BmdlVWLbekpKByZIFC09Qn8Wdhik2eINiwunBxPs=
|
||||||
apigo.cc/go/safe v1.5.3/go.mod h1:Ay8kEPL76DeXH4ifsVTc/3/sfGHlWLQjAp4vi7GA9AI=
|
apigo.cc/go/safe v1.5.3/go.mod h1:Ay8kEPL76DeXH4ifsVTc/3/sfGHlWLQjAp4vi7GA9AI=
|
||||||
apigo.cc/go/shell v1.5.5 h1:sf1QZiL7IHkoOLdrYX1qT4JWIlYZIAAYGJJXorjscB4=
|
apigo.cc/go/shell v1.5.3 h1:pI+u12sy6upoygq+1XXqUlvUboBfH4Q52jRpoJFv56A=
|
||||||
apigo.cc/go/shell v1.5.5/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
|
apigo.cc/go/shell v1.5.3/go.mod h1:FdZWUrcXHGJXo725oSyHqAeFoX0E9yY3PDhrz9hujgY=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
|||||||
15
js_export.go
15
js_export.go
@ -14,7 +14,7 @@ func init() {
|
|||||||
"RegisterAction": RegisterAction,
|
"RegisterAction": RegisterAction,
|
||||||
"RegisterSigner": registerSigner,
|
"RegisterSigner": registerSigner,
|
||||||
"Encrypt": Encrypt,
|
"Encrypt": Encrypt,
|
||||||
}, "SetConfig", "RegisterAction", "RegisterSigner")
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// call 提供给 JS 的私有入口
|
// call 提供给 JS 的私有入口
|
||||||
@ -28,11 +28,11 @@ func call(ctx context.Context, name string, payload any, options ...*CallOptions
|
|||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if res != nil {
|
if res != nil {
|
||||||
return resultMap(res), nil
|
return res, nil
|
||||||
}
|
}
|
||||||
return nil, jsmod.MakeError(err)
|
return nil, jsmod.MakeError(err)
|
||||||
}
|
}
|
||||||
return resultMap(res), nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionUsesDataResult(name string) bool {
|
func actionUsesDataResult(name string) bool {
|
||||||
@ -55,18 +55,11 @@ func projectDataResult(result *Result) any {
|
|||||||
if _, exists := output["error"]; !exists {
|
if _, exists := output["error"]; !exists {
|
||||||
output["error"] = result.Error
|
output["error"] = result.Error
|
||||||
}
|
}
|
||||||
if result.Timing != nil {
|
|
||||||
output["timing"] = result.Timing
|
|
||||||
}
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
output := map[string]any{
|
return map[string]any{
|
||||||
"ok": result.Ok, "code": result.Code, "error": result.Error, "result": result.Data,
|
"ok": result.Ok, "code": result.Code, "error": result.Error, "result": result.Data,
|
||||||
}
|
}
|
||||||
if result.Timing != nil {
|
|
||||||
output["timing"] = result.Timing
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerSigner 允许从 JS 注册动态签名逻辑
|
// registerSigner 允许从 JS 注册动态签名逻辑
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"apigo.cc/go/jsmod"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestProjectDataResult(t *testing.T) {
|
func TestProjectDataResult(t *testing.T) {
|
||||||
result := &Result{Ok: true, StatusCode: 200, Headers: map[string]string{"X-Test": "ignored"}, Data: map[string]any{"result": "hello"}, Timing: map[string]any{"unit": "ms", "total": int64(12)}}
|
result := &Result{Ok: true, StatusCode: 200, Headers: map[string]string{"X-Test": "ignored"}, Data: map[string]any{"result": "hello"}}
|
||||||
projected := projectDataResult(result).(map[string]any)
|
projected := projectDataResult(result).(map[string]any)
|
||||||
if projected["ok"] != true || projected["result"] != "hello" || projected["code"] != "" || projected["error"] != "" {
|
if projected["ok"] != true || projected["result"] != "hello" || projected["code"] != "" || projected["error"] != "" {
|
||||||
t.Fatalf("unexpected projected result: %#v", projected)
|
t.Fatalf("unexpected projected result: %#v", projected)
|
||||||
@ -15,19 +11,6 @@ func TestProjectDataResult(t *testing.T) {
|
|||||||
if _, exists := projected["headers"]; exists {
|
if _, exists := projected["headers"]; exists {
|
||||||
t.Fatalf("transport headers leaked into projected result: %#v", projected)
|
t.Fatalf("transport headers leaked into projected result: %#v", projected)
|
||||||
}
|
}
|
||||||
if projected["timing"].(map[string]any)["total"] != int64(12) {
|
|
||||||
t.Fatalf("timing was not projected: %#v", projected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResultMapUsesLowerCamelKeys(t *testing.T) {
|
|
||||||
mapped := resultMap(&Result{Ok: true, StatusCode: 200, Timing: map[string]any{"unit": "ms"}})
|
|
||||||
if mapped["ok"] != true || mapped["statusCode"] != 200 || mapped["timing"] == nil {
|
|
||||||
t.Fatalf("unexpected result map: %#v", mapped)
|
|
||||||
}
|
|
||||||
if _, exists := mapped["Ok"]; exists {
|
|
||||||
t.Fatalf("Go field name leaked into result map: %#v", mapped)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActionUsesInheritedDataResult(t *testing.T) {
|
func TestActionUsesInheritedDataResult(t *testing.T) {
|
||||||
@ -39,12 +22,3 @@ func TestActionUsesInheritedDataResult(t *testing.T) {
|
|||||||
t.Fatal("child Action did not inherit resultMode=data")
|
t.Fatal("child Action did not inherit resultMode=data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSFilterRegistrationIsUnsafe(t *testing.T) {
|
|
||||||
module := jsmod.GetModules()["api"]
|
|
||||||
for _, name := range []string{"SetConfig", "RegisterAction", "RegisterSigner"} {
|
|
||||||
if module == nil || !module.UnsafeList[name] {
|
|
||||||
t.Fatalf("JS API mutation %s must be blocked in safe mode", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ type CallOptions struct {
|
|||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
Stream *StreamOptions
|
Stream *StreamOptions
|
||||||
Timing bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamOptions receives the upstream response synchronously on the calling goroutine.
|
// StreamOptions receives the upstream response synchronously on the calling goroutine.
|
||||||
|
|||||||
109
yaml.go
109
yaml.go
@ -1,109 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseActionYAML parses a data-driven Action definition without applying
|
|
||||||
// product-specific defaults or registering it globally.
|
|
||||||
func ParseActionYAML(source string) (map[string]any, error) {
|
|
||||||
definition := map[string]any{}
|
|
||||||
if err := yaml.Unmarshal([]byte(source), &definition); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid Action YAML: %w", err)
|
|
||||||
}
|
|
||||||
if definition == nil {
|
|
||||||
return nil, fmt.Errorf("Action YAML must be an object")
|
|
||||||
}
|
|
||||||
return definition, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PatchActionTokens replaces the top-level tokens field while preserving the
|
|
||||||
// rest of the YAML document, including comments and field order.
|
|
||||||
func PatchActionTokens(source string, tokens []string) (string, error) {
|
|
||||||
document, root, err := parseActionYAMLDocument(source)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
index := yamlMappingIndex(root, "tokens")
|
|
||||||
if len(tokens) == 0 {
|
|
||||||
if index >= 0 {
|
|
||||||
root.Content = append(root.Content[:index], root.Content[index+2:]...)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sequence := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Style: yaml.FlowStyle}
|
|
||||||
for _, token := range tokens {
|
|
||||||
sequence.Content = append(sequence.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: token})
|
|
||||||
}
|
|
||||||
if index >= 0 {
|
|
||||||
root.Content[index+1] = sequence
|
|
||||||
} else {
|
|
||||||
root.Content = append(root.Content,
|
|
||||||
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "tokens"}, sequence)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return encodeActionYAML(document)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertActionJSONToYAML converts a legacy JSON Action document to readable
|
|
||||||
// block-style YAML. It does not add defaults or modify the Action semantics.
|
|
||||||
func ConvertActionJSONToYAML(source string) (string, error) {
|
|
||||||
document, root, err := parseActionYAMLDocument(source)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
useYAMLBlockStyle(root)
|
|
||||||
root.HeadComment = "API Action configuration. Field order and comments are preserved."
|
|
||||||
return encodeActionYAML(document)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseActionYAMLDocument(source string) (*yaml.Node, *yaml.Node, error) {
|
|
||||||
var document yaml.Node
|
|
||||||
if err := yaml.Unmarshal([]byte(source), &document); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("invalid Action YAML: %w", err)
|
|
||||||
}
|
|
||||||
if len(document.Content) == 0 || document.Content[0].Kind != yaml.MappingNode {
|
|
||||||
return nil, nil, fmt.Errorf("Action YAML must be an object")
|
|
||||||
}
|
|
||||||
return &document, document.Content[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeActionYAML(document *yaml.Node) (string, error) {
|
|
||||||
var output bytes.Buffer
|
|
||||||
encoder := yaml.NewEncoder(&output)
|
|
||||||
encoder.SetIndent(2)
|
|
||||||
if err := encoder.Encode(document); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
_ = encoder.Close()
|
|
||||||
return strings.TrimRight(output.String(), "\n") + "\n", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func yamlMappingIndex(root *yaml.Node, key string) int {
|
|
||||||
for index := 0; index+1 < len(root.Content); index += 2 {
|
|
||||||
if root.Content[index].Value == key {
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func useYAMLBlockStyle(node *yaml.Node) {
|
|
||||||
if node.Kind == yaml.MappingNode || node.Kind == yaml.SequenceNode {
|
|
||||||
node.Style = 0
|
|
||||||
}
|
|
||||||
if node.Kind == yaml.ScalarNode {
|
|
||||||
node.Style = 0
|
|
||||||
}
|
|
||||||
if node.Kind == yaml.MappingNode {
|
|
||||||
for index := 0; index < len(node.Content); index += 2 {
|
|
||||||
node.Content[index].Style = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, child := range node.Content {
|
|
||||||
useYAMLBlockStyle(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
53
yaml_test.go
53
yaml_test.go
@ -1,53 +0,0 @@
|
|||||||
package api_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"apigo.cc/go/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseActionYAML(t *testing.T) {
|
|
||||||
definition, err := api.ParseActionYAML("name: sample\nmethod: POST\nstream: false\n")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if definition["name"] != "sample" || definition["method"] != "POST" || definition["stream"] != false {
|
|
||||||
t.Fatalf("unexpected definition: %#v", definition)
|
|
||||||
}
|
|
||||||
if _, err = api.ParseActionYAML("- invalid\n"); err == nil {
|
|
||||||
t.Fatal("sequence Action must be rejected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPatchActionTokensPreservesDocument(t *testing.T) {
|
|
||||||
source := "# action comment\nname: sample\n# endpoint comment\nurl: https://example.com\ntokens: [old]\n"
|
|
||||||
updated, err := api.PatchActionTokens(source, []string{"one", "two"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, expected := range []string{"# action comment", "# endpoint comment", "name: sample", "tokens: [one, two]"} {
|
|
||||||
if !strings.Contains(updated, expected) {
|
|
||||||
t.Fatalf("patched YAML does not contain %q:\n%s", expected, updated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
removed, err := api.PatchActionTokens(updated, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if strings.Contains(removed, "tokens:") {
|
|
||||||
t.Fatalf("tokens field was not removed:\n%s", removed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertActionJSONToYAML(t *testing.T) {
|
|
||||||
converted, err := api.ConvertActionJSONToYAML(`{"name":"sample","headers":{"Accept":"application/json"},"filters":["clean"]}`)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, expected := range []string{"# API Action configuration", "name: sample", "headers:\n Accept: application/json", "filters:\n - clean"} {
|
|
||||||
if !strings.Contains(converted, expected) {
|
|
||||||
t.Fatalf("converted YAML does not contain %q:\n%s", expected, converted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user