Skip to content
Closed
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
57 changes: 57 additions & 0 deletions sdk/go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,3 +661,60 @@ func TestHealth(t *testing.T) {
t.Fatal("expected healthy")
}
}

func TestRelationsLinkUnlink(t *testing.T) {
client, closeServer := testClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/relations":
if got := r.URL.Query().Get("uri"); got != "viking://resources/a" {
t.Fatalf("relations uri query = %q", got)
}
writeOK(t, w, []map[string]any{
{"uri": "viking://resources/b", "reason": "cites"},
})
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/relations/link":
body := readJSONBody(t, r)
if got := body["from_uri"]; got != "viking://resources/a" {
t.Fatalf("link from_uri = %#v", got)
}
toURIs, ok := body["to_uris"].([]any)
if !ok || len(toURIs) != 2 || toURIs[0] != "viking://resources/b" || toURIs[1] != "viking://resources/c" {
t.Fatalf("link to_uris = %#v", body["to_uris"])
}
if got := body["reason"]; got != "cites" {
t.Fatalf("link reason = %#v", got)
}
writeOK(t, w, map[string]any{"from": "viking://resources/a", "to": toURIs})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/relations/link":
body := readJSONBody(t, r)
if got := body["from_uri"]; got != "viking://resources/a" {
t.Fatalf("unlink from_uri = %#v", got)
}
if got := body["to_uri"]; got != "viking://resources/b" {
t.Fatalf("unlink to_uri = %#v", got)
}
writeOK(t, w, map[string]any{"from": "viking://resources/a", "to": "viking://resources/b"})
default:
t.Fatalf("unexpected %s %s", r.Method, r.URL.Path)
}
}))
defer closeServer()

rels, err := client.Relations(context.Background(), "viking://resources/a")
if err != nil {
t.Fatal(err)
}
if len(rels) != 1 || rels[0]["uri"] != "viking://resources/b" || rels[0]["reason"] != "cites" {
t.Fatalf("unexpected relations: %#v", rels)
}

// Short URIs are normalized to viking:// form, and a single target is sent as a list.
if err := client.Link(context.Background(), "resources/a",
[]string{"resources/b", "resources/c"}, "cites"); err != nil {
t.Fatal(err)
}

if err := client.Unlink(context.Background(), "resources/a", "resources/b"); err != nil {
t.Fatal(err)
}
}
41 changes: 41 additions & 0 deletions sdk/go/relations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package openviking

import (
"context"
"net/http"
"net/url"
)

// Relations returns the relations recorded for a resource URI.
// Each entry mirrors the server payload, e.g. {"uri": "...", "reason": "..."}.
func (c *Client) Relations(ctx context.Context, uri string) ([]map[string]any, error) {
var result []map[string]any
err := c.doJSON(ctx, http.MethodGet, "/api/v1/relations",
url.Values{"uri": {NormalizeURI(uri)}}, nil, &result)
return result, err
}

// Link creates relations from one resource to one or more target resources.
// The server accepts a single target or a list (Union[str, List[str]]); the Go
// client always sends a list, so pass []string{target} to link a single URI.
func (c *Client) Link(ctx context.Context, fromURI string, toURIs []string, reason string) error {
normalized := make([]string, len(toURIs))
for i, u := range toURIs {
normalized[i] = NormalizeURI(u)
}
payload := map[string]any{
"from_uri": NormalizeURI(fromURI),
"to_uris": normalized,
"reason": reason,
}
return c.doJSON(ctx, http.MethodPost, "/api/v1/relations/link", nil, payload, nil)
}

// Unlink removes the relation between two resources.
func (c *Client) Unlink(ctx context.Context, fromURI, toURI string) error {
payload := map[string]any{
"from_uri": NormalizeURI(fromURI),
"to_uri": NormalizeURI(toURI),
}
return c.doJSON(ctx, http.MethodDelete, "/api/v1/relations/link", nil, payload, nil)
}
Loading